From 6590f6e50f7125de4fe0a1d04062c814c69cdd94 Mon Sep 17 00:00:00 2001 From: Preston Vasquez Date: Wed, 10 Sep 2025 14:09:23 -0600 Subject: [PATCH 01/13] Allow appending client metadata --- .../assertbsoncore/assertions_bsoncore.go | 32 ++ internal/integration/client_test.go | 79 ++- internal/integration/handshake_test.go | 530 ++++++++++++++---- internal/integration/mtest/mongotest.go | 45 +- internal/integration/mtest/proxy_capture.go | 53 ++ internal/integration/mtest/proxy_dialer.go | 31 +- internal/integration/sdam_prose_test.go | 2 +- .../unified/client_operation_execution.go | 29 + internal/integration/unified/operation.go | 4 +- .../integration/unified/unified_spec_test.go | 1 + internal/test/client_metadata.go | 206 +++++++ mongo/client.go | 100 +++- x/mongo/driver/topology/server.go | 13 +- x/mongo/driver/topology/server_options.go | 33 +- x/mongo/driver/topology/topology_options.go | 89 ++- .../driver/topology/topology_options_test.go | 2 +- 16 files changed, 985 insertions(+), 264 deletions(-) create mode 100644 internal/assert/assertbsoncore/assertions_bsoncore.go create mode 100644 internal/integration/mtest/proxy_capture.go create mode 100644 internal/test/client_metadata.go diff --git a/internal/assert/assertbsoncore/assertions_bsoncore.go b/internal/assert/assertbsoncore/assertions_bsoncore.go new file mode 100644 index 0000000000..2a47a243b5 --- /dev/null +++ b/internal/assert/assertbsoncore/assertions_bsoncore.go @@ -0,0 +1,32 @@ +// Copyright (C) MongoDB, Inc. 2025-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 assertbsoncore + +import ( + "testing" + + "go.mongodb.org/mongo-driver/v2/internal/assert" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" +) + +func HandshakeClientMetadata(t testing.TB, expectedWM, actualWM []byte) bool { + command := bsoncore.Document(actualWM) + + // Lookup the "client" field in the command document. + clientVal, err := command.LookupErr("client") + if err != nil { + return assert.Fail(t, "expected command to contain the 'client' field") + } + + GotCommand, ok := clientVal.DocumentOK() + if !ok { + return assert.Fail(t, "expected client field to be a document, got %s", clientVal.Type) + } + + wantCommand := bsoncore.Document(expectedWM) + return assert.Equal(t, wantCommand, GotCommand, "want: %v, got: %v", wantCommand, GotCommand) +} diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 8b37f12b47..0bf0e3ecd6 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -19,12 +19,13 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/internal/assert" + "go.mongodb.org/mongo-driver/v2/internal/assert/assertbsoncore" "go.mongodb.org/mongo-driver/v2/internal/eventtest" "go.mongodb.org/mongo-driver/v2/internal/failpoint" - "go.mongodb.org/mongo-driver/v2/internal/handshake" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" "go.mongodb.org/mongo-driver/v2/internal/integtest" "go.mongodb.org/mongo-driver/v2/internal/require" + "go.mongodb.org/mongo-driver/v2/internal/test" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/mongo/readpref" @@ -456,26 +457,12 @@ func TestClient(t *testing.T) { err := mt.Client.Ping(context.Background(), mtest.PrimaryRp) assert.Nil(mt, err, "Ping error: %v", err) - msgPairs := mt.GetProxiedMessages() - assert.True(mt, len(msgPairs) >= 2, "expected at least 2 events sent, got %v", len(msgPairs)) + want := test.EncodeClientMetadata(mt, test.WithClientMentadataAppName("foo")) + for i := 0; i < 2; i++ { + message := mt.GetProxyCapture().TryNext() + require.NotNil(mt, message, "expected handshake message, got nil") - // First two messages should be connection handshakes: one for the heartbeat connection and the other for the - // application connection. - for idx, pair := range msgPairs[:2] { - helloCommand := handshake.LegacyHello - // Expect "hello" command name with API version. - if os.Getenv("REQUIRE_API_VERSION") == "true" { - helloCommand = "hello" - } - assert.Equal(mt, pair.CommandName, helloCommand, "expected command name %s at index %d, got %s", helloCommand, idx, - pair.CommandName) - - sent := pair.Sent - appNameVal, err := sent.Command.LookupErr("client", "application", "name") - assert.Nil(mt, err, "expected command %s at index %d to contain app name", sent.Command, idx) - appName := appNameVal.StringValue() - assert.Equal(mt, testAppName, appName, "expected app name %v at index %d, got %v", testAppName, idx, - appName) + assertbsoncore.HandshakeClientMetadata(mt, want, message.Sent.Command) } }) @@ -604,24 +591,32 @@ func TestClient(t *testing.T) { err := mt.Client.Ping(context.Background(), mtest.PrimaryRp) assert.Nil(mt, err, "Ping error: %v", err) - msgPairs := mt.GetProxiedMessages() - assert.True(mt, len(msgPairs) >= 3, "expected at least 3 events, got %v", len(msgPairs)) + proxyCapture := mt.GetProxyCapture() // The first message should be a connection handshake. - pair := msgPairs[0] - assert.Equal(mt, handshake.LegacyHello, pair.CommandName, "expected command name %s at index 0, got %s", - handshake.LegacyHello, pair.CommandName) - assert.Equal(mt, wiremessage.OpQuery, pair.Sent.OpCode, - "expected 'OP_QUERY' OpCode in wire message, got %q", pair.Sent.OpCode.String()) - - // Look for a saslContinue in the remaining proxied messages and assert that it uses the OP_MSG OpCode, as wire - // version is now known to be >= 6. + firstMessage := proxyCapture.TryNext() + require.NotNil(mt, firstMessage, "expected handshake message, got nil") + + assert.True(t, firstMessage.IsHandshake()) + + opCode := firstMessage.Sent.OpCode + assert.Equal(mt, wiremessage.OpQuery, opCode, + "expected 'OP_MSG' OpCode in wire message, got %q", opCode.String()) + + // Look for a saslContinue in the remaining proxied messages and assert that + // it uses the OP_MSG OpCode, as wire version is now known to be >= 6. var saslContinueFound bool - for _, pair := range msgPairs[1:] { - if pair.CommandName == "saslContinue" { + for { + message := proxyCapture.TryNext() + if message == nil { + break + } + + if message.CommandName == "saslContinue" { saslContinueFound = true - assert.Equal(mt, wiremessage.OpMsg, pair.Sent.OpCode, - "expected 'OP_MSG' OpCode in wire message, got %s", pair.Sent.OpCode.String()) + opCode := message.Sent.OpCode + assert.Equal(mt, wiremessage.OpMsg, opCode, + "expected 'OP_MSG' OpCode in wire message, got %q", opCode.String()) break } } @@ -634,18 +629,18 @@ func TestClient(t *testing.T) { err := mt.Client.Ping(context.Background(), mtest.PrimaryRp) assert.Nil(mt, err, "Ping error: %v", err) - msgPairs := mt.GetProxiedMessages() - assert.True(mt, len(msgPairs) >= 3, "expected at least 3 events, got %v", len(msgPairs)) - // First three messages should be connection handshakes: one for the heartbeat connection, another for the // application connection, and a final one for the RTT monitor connection. - for idx, pair := range msgPairs[:3] { - assert.Equal(mt, "hello", pair.CommandName, "expected command name 'hello' at index %d, got %s", idx, - pair.CommandName) + for idx := 0; idx < 3; idx++ { + message := mt.GetProxyCapture().TryNext() + require.NotNil(mt, message, "expected handshake message, got nil") + + assert.True(t, message.IsHandshake()) // Assert that appended OpCode is OP_MSG when API version is set. - assert.Equal(mt, wiremessage.OpMsg, pair.Sent.OpCode, - "expected 'OP_MSG' OpCode in wire message, got %q", pair.Sent.OpCode.String()) + opCode := message.Sent.OpCode + assert.Equal(mt, wiremessage.OpMsg, opCode, + "expected 'OP_MSG' OpCode in wire message, got %q", opCode.String()) } }) diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index f4c449e30e..628443d331 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -9,17 +9,16 @@ package integration import ( "context" "os" - "reflect" - "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/handshake" + "go.mongodb.org/mongo-driver/v2/internal/assert/assertbsoncore" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" + "go.mongodb.org/mongo-driver/v2/internal/ptrutil" "go.mongodb.org/mongo-driver/v2/internal/require" + "go.mongodb.org/mongo-driver/v2/internal/test" "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" ) @@ -35,40 +34,6 @@ func TestHandshakeProse(t *testing.T) { CreateCollection(false). ClientType(mtest.Proxy) - clientMetadata := func(env bson.D, info *options.DriverInfo) bson.D { - var ( - driverName = "mongo-go-driver" - driverVersion = version.Driver - platform = runtime.Version() - ) - - if info != nil { - driverName = driverName + "|" + info.Name - driverVersion = driverVersion + "|" + info.Version - platform = platform + "|" + info.Platform - } - - elems := bson.D{ - {Key: "driver", Value: bson.D{ - {Key: "name", Value: driverName}, - {Key: "version", Value: driverVersion}, - }}, - {Key: "os", Value: bson.D{ - {Key: "type", Value: runtime.GOOS}, - {Key: "architecture", Value: runtime.GOARCH}, - }}, - } - - elems = append(elems, bson.E{Key: "platform", Value: platform}) - - // If env is empty, don't include it in the metadata. - if env != nil && !reflect.DeepEqual(env, bson.D{}) { - elems = append(elems, bson.E{Key: "env", Value: env}) - } - - return elems - } - driverInfo := &options.DriverInfo{ Name: "outer-library-name", Version: "outer-library-version", @@ -88,11 +53,11 @@ func TestHandshakeProse(t *testing.T) { t.Setenv("FUNCTION_REGION", "") t.Setenv("VERCEL_REGION", "") - for _, test := range []struct { + testCases := []struct { name string env map[string]string opts *options.ClientOptions - want bson.D + want []byte }{ { name: "1. valid AWS", @@ -102,11 +67,11 @@ func TestHandshakeProse(t *testing.T) { "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "aws.lambda"}, - {Key: "memory_mb", Value: 1024}, - {Key: "region", Value: "us-east-2"}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("aws.lambda"), + test.WithClientMetadataEnvMemoryMB(ptrutil.Ptr(1024)), + test.WithClientMetadataEnvRegion("us-east-2"), + ), }, { name: "2. valid Azure", @@ -114,9 +79,9 @@ func TestHandshakeProse(t *testing.T) { "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "azure.func"}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("azure.func"), + ), }, { name: "3. valid GCP", @@ -127,12 +92,12 @@ func TestHandshakeProse(t *testing.T) { "FUNCTION_REGION": "us-central1", }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "gcp.func"}, - {Key: "memory_mb", Value: 1024}, - {Key: "region", Value: "us-central1"}, - {Key: "timeout_sec", Value: 60}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("gcp.func"), + test.WithClientMetadataEnvMemoryMB(ptrutil.Ptr(1024)), + test.WithClientMetadataEnvRegion("us-central1"), + test.WithClientMetadataEnvTimeoutSec(ptrutil.Ptr(60)), + ), }, { name: "4. valid Vercel", @@ -141,10 +106,10 @@ func TestHandshakeProse(t *testing.T) { "VERCEL_REGION": "cdg1", }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "vercel"}, - {Key: "region", Value: "cdg1"}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("vercel"), + test.WithClientMetadataEnvRegion("cdg1"), + ), }, { name: "5. invalid multiple providers", @@ -153,7 +118,7 @@ func TestHandshakeProse(t *testing.T) { "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, - want: clientMetadata(nil, nil), + want: test.EncodeClientMetadata(mt), }, { name: "6. invalid long string", @@ -168,9 +133,9 @@ func TestHandshakeProse(t *testing.T) { }(), }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "aws.lambda"}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("aws.lambda"), + ), }, { name: "7. invalid wrong types", @@ -179,9 +144,9 @@ func TestHandshakeProse(t *testing.T) { "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big", }, opts: nil, - want: clientMetadata(bson.D{ - {Key: "name", Value: "aws.lambda"}, - }, nil), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataEnvName("aws.lambda"), + ), }, { name: "8. Invalid - AWS_EXECUTION_ENV does not start with \"AWS_Lambda_\"", @@ -189,51 +154,38 @@ func TestHandshakeProse(t *testing.T) { "AWS_EXECUTION_ENV": "EC2", }, opts: nil, - want: clientMetadata(nil, nil), + want: test.EncodeClientMetadata(mt), }, { name: "driver info included", opts: options.Client().SetDriverInfo(driverInfo), - want: clientMetadata(nil, driverInfo), + want: test.EncodeClientMetadata(mt, + test.WithClientMetadataDriverName("outer-library-name"), + test.WithClientMetadataDriverVersion("outer-library-version"), + test.WithClientMetadataDriverPlatform("outer-library-platform"), + ), }, - } { - test := test + } - mt.RunOpts(test.name, opts, func(mt *mtest.T) { - for k, v := range test.env { + for _, tc := range testCases { + mt.RunOpts(tc.name, opts, func(mt *mtest.T) { + for k, v := range tc.env { mt.Setenv(k, v) } - if test.opts != nil { - mt.ResetClient(test.opts) + 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) - messages := mt.GetProxiedMessages() - handshakeMessage := messages[:1][0] - - hello := handshake.LegacyHello - if os.Getenv("REQUIRE_API_VERSION") == "true" { - hello = "hello" - } - - assert.Equal(mt, hello, handshakeMessage.CommandName) + firstMessage := mt.GetProxyCapture().TryNext() + require.NotNil(mt, firstMessage, "expected to capture a proxied message") - // Lookup the "client" field in the command document. - clientVal, err := handshakeMessage.Sent.Command.LookupErr("client") - require.NoError(mt, err, "expected command %s to contain client field", handshakeMessage.Sent.Command) - - got, ok := clientVal.DocumentOK() - require.True(mt, ok, "expected client field to be a document, got %s", clientVal.Type) - - wantBytes, err := bson.Marshal(test.want) - require.NoError(mt, err, "error marshaling want document: %v", err) - - want := bsoncore.Document(wantBytes) - assert.Equal(mt, want, got, "want: %v, got: %v", want, got) + assert.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") + assertbsoncore.HandshakeClientMetadata(mt, tc.want, firstMessage.Sent.Command) }) } } @@ -249,13 +201,13 @@ func TestLoadBalancedConnectionHandshake(t *testing.T) { err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) - messages := mt.GetProxiedMessages() - handshakeMessage := messages[:1][0] + 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", handshakeMessage.CommandName) - assert.Equal(mt, wiremessage.OpMsg, handshakeMessage.Sent.OpCode) + assert.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") + assert.Equal(mt, wiremessage.OpMsg, firstMessage.Sent.OpCode) }) opts := mtest.NewOptions().ClientType(mtest.Proxy).Topologies( @@ -269,21 +221,385 @@ func TestLoadBalancedConnectionHandshake(t *testing.T) { err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) - messages := mt.GetProxiedMessages() - handshakeMessage := messages[:1][0] + 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, handshakeMessage.CommandName) - assert.Equal(mt, want, handshakeMessage.Sent.OpCode) + assert.True(mt, firstMessage.IsHandshake(), "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 options.DriverInfo + + // 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: options.DriverInfo{ + Name: "library|framework", + Version: "1.2|2.0", + Platform: "Library Platform|Framework Platform", + }, + append: false, + }, + { + name: "test1.2: append with no platform", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "2.0", + Platform: "", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2|2.0", + Platform: "Library Platform", + }, + append: false, + }, + { + name: "test1.3: append with no version", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "", + Platform: "Framework Platform", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "Library Platform|Framework Platform", + }, + append: false, + }, + { + name: "test1.4: append with name only", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "", + Platform: "", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "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: options.DriverInfo{ + Name: "library|framework", + Version: "1.2|2.0", + Platform: "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: options.DriverInfo{ + Name: "library|framework", + Version: "1.2|2.0", + Platform: "Library Platform", + }, + append: true, + }, + { + name: "test2.3: append with no version after appending", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "", + Platform: "Framework Platform", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "Library Platform|Framework Platform", + }, + append: true, + }, + { + name: "test2.4: append with name only after appending", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "", + Platform: "", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "Library Platform", + }, + append: true, + }, + { + name: "test3.1: same driver info after appending", + driverInfo: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "Library Platform", + }, + want: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "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: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "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: options.DriverInfo{ + Name: "library", + Version: "1.2|2.0", + Platform: "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: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "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: options.DriverInfo{ + Name: "library|framework", + Version: "1.2|2.0", + Platform: "Library Platform", + }, + append: true, + }, + { + name: "test3.6: same version after appending", + driverInfo: options.DriverInfo{ + Name: "framework", + Version: "1.2", + Platform: "Framework Platform", + }, + want: options.DriverInfo{ + Name: "library|framework", + Version: "1.2", + Platform: "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: options.DriverInfo{ + Name: "library", + Version: "1.2|2.0", + Platform: "Library Platform|Framework Platform", + }, + append: true, + }, + } + + for _, tc := range testCases { + // 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(20 * 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") + + want := test.EncodeClientMetadata(mt, + test.WithClientMetadataDriverName(tc.want.Name), + test.WithClientMetadataDriverVersion(tc.want.Version), + test.WithClientMetadataDriverPlatform(tc.want.Platform), + ) + + assertbsoncore.HandshakeClientMetadata(mt, want, gotMessage.Sent.Command) + }) + } +} + +// Test 4: Multiple Metadata Updates with Duplicate Dataa +func TestHandshakeProse_AppendMetadata_Test4(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`. + clientMetadata := mt.GetProxyCapture().TryNext() + + require.NotNil(mt, clientMetadata, "expected to capture a proxied message") + assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + + // 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") + + // 12. Assert that `clientMetadata` is identical to `updatedClientMetadata`. + want := bsoncore.Document(clientMetadata.Sent.Command) + got := bsoncore.Document(updatedClientMetadata.Sent.Command) + + assert.Equal(mt, want, got, "expected: %s, got: %s", want, got) } diff --git a/internal/integration/mtest/mongotest.go b/internal/integration/mtest/mongotest.go index ce9823b89a..2afdd6f4f1 100644 --- a/internal/integration/mtest/mongotest.go +++ b/internal/integration/mtest/mongotest.go @@ -60,24 +60,25 @@ type T struct { *testing.T // members for only this T instance - createClient *bool - createCollection *bool - runOn []RunOnBlock - mockDeployment *drivertest.MockDeployment // nil if the test is not being run against a mock - mockResponses []bson.D - createdColls []*Collection // collections created in this test - proxyDialer *proxyDialer - dbName, collName string - failPointNames []string - minServerVersion string - maxServerVersion string - validTopologies []TopologyKind - auth *bool - enterprise *bool - dataLake *bool - ssl *bool - collCreateOpts *options.CreateCollectionOptionsBuilder - requireAPIVersion *bool + createClient *bool + createCollection *bool + runOn []RunOnBlock + mockDeployment *drivertest.MockDeployment // nil if the test is not being run against a mock + mockResponses []bson.D + createdColls []*Collection // collections created in this test + proxyDialer *proxyDialer + proxyDialerHandshakeQueue <-chan *ProxyMessage // FIFO queue of handshake messages sent by the proxy dialer + dbName, collName string + failPointNames []string + minServerVersion string + maxServerVersion string + validTopologies []TopologyKind + auth *bool + enterprise *bool + dataLake *bool + ssl *bool + collCreateOpts *options.CreateCollectionOptionsBuilder + requireAPIVersion *bool // options copied to sub-tests clientType ClientType @@ -338,13 +339,13 @@ func (t *T) FilterFailedEvents(filter func(*event.CommandFailedEvent) bool) { t.failed = newEvents } -// GetProxiedMessages returns the messages proxied to the server by the test. If the client type is not Proxy, this -// returns nil. -func (t *T) GetProxiedMessages() []*ProxyMessage { +// GetProxyCapture returns the ProxyCapture used by the test. If the client +// type is not Proxy, this returns nil. +func (t *T) GetProxyCapture() *ProxyCapture { if t.proxyDialer == nil { return nil } - return t.proxyDialer.Messages() + return t.proxyDialer.proxyCapture } // NumberConnectionsCheckedOut returns the number of connections checked out from the test Client. diff --git a/internal/integration/mtest/proxy_capture.go b/internal/integration/mtest/proxy_capture.go new file mode 100644 index 0000000000..5cd43cc984 --- /dev/null +++ b/internal/integration/mtest/proxy_capture.go @@ -0,0 +1,53 @@ +// Copyright (C) MongoDB, Inc. 2025-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 mtest + +import ( + "sync" +) + +// ProxyCapture provides a FIFO channel for handshake messages passed +// through the mtest proxyDialer. +type ProxyCapture struct { + messages chan *ProxyMessage + mu sync.Mutex +} + +func newProxyCapture(bufferSize int) *ProxyCapture { + return &ProxyCapture{ + messages: make(chan *ProxyMessage, bufferSize), + } +} + +func (hc *ProxyCapture) Capture(msg *ProxyMessage) { + hc.mu.Lock() + defer hc.mu.Unlock() + + hc.messages <- msg +} + +func (hc *ProxyCapture) TryNext() *ProxyMessage { + select { + case msg := <-hc.messages: + return msg + default: + return nil + } +} + +// Drain removes all messages from the channel and returns them as a slice. +func (hc *ProxyCapture) Drain() []*ProxyMessage { + messages := []*ProxyMessage{} + for { + select { + case msg := <-hc.messages: + messages = append(messages, msg) + default: + return messages + } + } +} diff --git a/internal/integration/mtest/proxy_dialer.go b/internal/integration/mtest/proxy_dialer.go index 7f17dbbdb1..8e750500eb 100644 --- a/internal/integration/mtest/proxy_dialer.go +++ b/internal/integration/mtest/proxy_dialer.go @@ -11,9 +11,11 @@ import ( "errors" "fmt" "net" + "os" "sync" "time" + "go.mongodb.org/mongo-driver/v2/internal/handshake" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) @@ -32,7 +34,7 @@ type proxyDialer struct { *net.Dialer sync.Mutex - messages []*ProxyMessage + //messages []*ProxyMessage // sentMap temporarily stores the message sent to the server using the requestID so it can map requests to their // responses. sentMap sync.Map @@ -40,13 +42,16 @@ type proxyDialer struct { // differ. This can happen if a connection is dialed to a host name, in which case the reported remote address will // be the resolved IP address. addressTranslations sync.Map + + proxyCapture *ProxyCapture } var _ options.ContextDialer = (*proxyDialer)(nil) func newProxyDialer() *proxyDialer { return &proxyDialer{ - Dialer: &net.Dialer{Timeout: 30 * time.Second}, + Dialer: &net.Dialer{Timeout: 30 * time.Second}, + proxyCapture: newProxyCapture(100), } } @@ -121,21 +126,10 @@ func (p *proxyDialer) storeReceivedMessage(wm []byte, addr string) error { Sent: sent, Received: parsed, } - p.messages = append(p.messages, msgPair) + p.proxyCapture.Capture(msgPair) return nil } -// Messages returns a slice of proxied messages. This slice is a copy of the messages proxied so far and will not be -// updated for messages proxied after this call. -func (p *proxyDialer) Messages() []*ProxyMessage { - p.Lock() - defer p.Unlock() - - copiedMessages := make([]*ProxyMessage, len(p.messages)) - copy(copiedMessages, p.messages) - return copiedMessages -} - // proxyConn is a net.Conn that wraps a network connection. All messages sent/received through a proxyConn are stored // in the associated proxyDialer and are forwarded over the wrapped connection. Errors encountered when parsing and // storing wire messages are wrapped to add context, while errors returned from the underlying network connection are @@ -184,3 +178,12 @@ func (pc *proxyConn) Read(buffer []byte) (int, error) { return n, nil } + +func (msg *ProxyMessage) IsHandshake() bool { + hello := handshake.LegacyHello + if os.Getenv("REQUIRE_API_VERSION") == "true" { + hello = "hello" + } + + return hello == msg.CommandName +} diff --git a/internal/integration/sdam_prose_test.go b/internal/integration/sdam_prose_test.go index 274d6c0abb..ac9572ac02 100644 --- a/internal/integration/sdam_prose_test.go +++ b/internal/integration/sdam_prose_test.go @@ -69,7 +69,7 @@ func TestSDAMProse(t *testing.T) { } start := time.Now() time.Sleep(2 * time.Second) - messages := mt.GetProxiedMessages() + messages := mt.GetProxyCapture().Drain() duration := time.Since(start) hosts, err := mongoutil.HostsFromURI(mtest.ClusterURI()) diff --git a/internal/integration/unified/client_operation_execution.go b/internal/integration/unified/client_operation_execution.go index 86f161761d..25d42743e6 100644 --- a/internal/integration/unified/client_operation_execution.go +++ b/internal/integration/unified/client_operation_execution.go @@ -307,6 +307,35 @@ func executeClientBulkWrite(ctx context.Context, operation *operation) (*operati return newDocumentResult(rawBuilder.Build(), err), nil } +func executeAppendMetadata(ctx context.Context, op *operation) (*operationResult, error) { + client, err := entities(ctx).client(op.Object) + if err != nil { + return nil, fmt.Errorf("error getting client entity: %w", err) + } + + elems, err := op.Arguments.Elements() + if err != nil { + return nil, fmt.Errorf("error getting appendMetadata arguments: %w", err) + } + + driverInfo := options.DriverInfo{} + for _, elem := range elems { + key := elem.Key() + val := elem.Value() + + switch key { + case "driverInfoOptions": + if err = bson.Unmarshal(val.Value, &driverInfo); err != nil { + return nil, fmt.Errorf("error unmarshaling driverInfoOptions: %w", err) + } + } + } + + client.AppendDriverInfo(driverInfo) + + return newEmptyResult(), nil +} + func createClientInsertOneModel(value bson.Raw) (*mongo.ClientBulkWrite, error) { var v struct { Namespace string diff --git a/internal/integration/unified/operation.go b/internal/integration/unified/operation.go index 9baf785dcb..1b591d66af 100644 --- a/internal/integration/unified/operation.go +++ b/internal/integration/unified/operation.go @@ -128,7 +128,9 @@ func (op *operation) run(ctx context.Context, loopDone <-chan struct{}) (*operat // executeWithTransaction internally verifies results/errors for each operation, so it doesn't return a result. return newEmptyResult(), executeWithTransaction(ctx, op, loopDone) - // Client operations + // Client operations + case "appendMetadata": + return executeAppendMetadata(ctx, op) case "createChangeStream": return executeCreateChangeStream(ctx, op) case "listDatabases": diff --git a/internal/integration/unified/unified_spec_test.go b/internal/integration/unified/unified_spec_test.go index 9021d03e75..03b4139f90 100644 --- a/internal/integration/unified/unified_spec_test.go +++ b/internal/integration/unified/unified_spec_test.go @@ -37,6 +37,7 @@ var ( "run-command/tests/unified", "index-management/tests", "atlas-data-lake-testing/tests/unified", + "mongodb-handshake/tests/unified", } failDirectories = []string{ "unified-test-format/tests/valid-fail", diff --git a/internal/test/client_metadata.go b/internal/test/client_metadata.go new file mode 100644 index 0000000000..a35c8fe5a4 --- /dev/null +++ b/internal/test/client_metadata.go @@ -0,0 +1,206 @@ +// Copyright (C) MongoDB, Inc. 2025-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 test + +import ( + "runtime" + "testing" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/require" + "go.mongodb.org/mongo-driver/v2/version" +) + +type clientMetadataOptions struct { + appName string + driverName string + driverVersion string + driverPlatform string + envName string + envTimeoutSec *int + envMemoryMB *int + envRegion string +} + +// ClientMetadataOption represents a configuration option for building client +// metadata. +type ClientMetadataOption func(*clientMetadataOptions) + +// Existing (note the typo in the function name). Kept for compatibility. +func WithClientMentadataAppName(name string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.appName = name + } +} + +// WithClientMetadataAppName sets the application name included in client metadata. +func WithClientMetadataAppName(name string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.appName = name + } +} + +// WithClientMetadataDriverName sets the driver name (e.g., "mongo-go-driver"). +func WithClientMetadataDriverName(name string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.driverName = name + } +} + +// WithClientMetadataDriverVersion sets the driver version (e.g., "1.16.0"). +func WithClientMetadataDriverVersion(version string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.driverVersion = version + } +} + +// WithClientMetadataDriverPlatform sets the driver platform string +// (e.g., "go1.22.5 gc linux/amd64"). +func WithClientMetadataDriverPlatform(platform string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.driverPlatform = platform + } +} + +// WithClientMetadataEnvName sets the execution environment name +// (e.g., "AWS Lambda", "GCP Cloud Functions", "Kubernetes"). +func WithClientMetadataEnvName(name string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.envName = name + } +} + +// WithClientMetadataEnvTimeoutSec sets the execution timeout in seconds. +// Pass nil to indicate "unspecified" or "not applicable". +func WithClientMetadataEnvTimeoutSec(timeoutSec *int) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.envTimeoutSec = timeoutSec + } +} + +// WithClientMetadataEnvMemoryMB sets the memory limit in megabytes. +// Pass nil to indicate "unspecified" or "not applicable". +func WithClientMetadataEnvMemoryMB(memoryMB *int) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.envMemoryMB = memoryMB + } +} + +// WithClientMetadataEnvRegion sets the deployment/region identifier +// (e.g., "us-east-1", "europe-west1"). +func WithClientMetadataEnvRegion(region string) ClientMetadataOption { + return func(o *clientMetadataOptions) { + o.envRegion = region + } +} + +// EncodeClientMetadata constructs the WM byte slice that represents the client +// metadata document for the given options with the intent of comparing to an +// actual handshake wire message: +// +// { +// application: { +// name: "" +// }, +// driver: { +// name: "", +// version: "" +// }, +// platform: "", +// os: { +// type: "", +// name: "", +// architecture: "", +// version: "" +// }, +// env: { +// name: "", +// timeout_sec: 42, +// memory_mb: 1024, +// region: "", +// container: { +// runtime: "", +// orchestrator: "" +// } +// } +// } +// +// This function was not put in mtest since it could be used in non-integration +// test conditions. +func EncodeClientMetadata(t testing.TB, opts ...ClientMetadataOption) []byte { + t.Helper() + + cfg := clientMetadataOptions{} + for _, apply := range opts { + apply(&cfg) + } + + var ( + driverName = "mongo-go-driver" // Default + driverVersion = version.Driver + platform = runtime.Version() // Default + ) + + if cfg.driverName != "" { + driverName = driverName + "|" + cfg.driverName + } + + if cfg.driverVersion != "" { + driverVersion = driverVersion + "|" + cfg.driverVersion + } + + if cfg.driverPlatform != "" { + platform = platform + "|" + cfg.driverPlatform + } + + elems := bson.D{} + + if cfg.appName != "" { + elems = append(elems, bson.E{Key: "application", Value: bson.D{ + {Key: "name", Value: cfg.appName}, + }}) + } + + elems = append(elems, bson.D{ + {Key: "driver", Value: bson.D{ + {Key: "name", Value: driverName}, + {Key: "version", Value: driverVersion}, + }}, + {Key: "os", Value: bson.D{ + {Key: "type", Value: runtime.GOOS}, + {Key: "architecture", Value: runtime.GOARCH}, + }}, + }...) + + elems = append(elems, bson.E{Key: "platform", Value: platform}) + + envElems := bson.D{} + if cfg.envName != "" { + envElems = append(envElems, bson.E{Key: "name", Value: cfg.envName}) + } + + if cfg.envMemoryMB != nil { + envElems = append(envElems, bson.E{Key: "memory_mb", Value: *cfg.envMemoryMB}) + } + + if cfg.envRegion != "" { + envElems = append(envElems, bson.E{Key: "region", Value: cfg.envRegion}) + } + + if cfg.envTimeoutSec != nil { + envElems = append(envElems, bson.E{Key: "timeout_sec", Value: *cfg.envTimeoutSec}) + } + + if len(envElems) > 0 { + elems = append(elems, bson.E{Key: "env", Value: envElems}) + } + + bytes, err := bson.Marshal(elems) + require.NoError(t, err) + + return bytes +} diff --git a/mongo/client.go b/mongo/client.go index f0480a0c72..4c781fbe55 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net/http" + "sync/atomic" "time" "go.mongodb.org/mongo-driver/v2/bson" @@ -56,24 +57,25 @@ var ( // The Client type opens and closes connections automatically and maintains a pool of idle connections. For // connection pool configuration options, see documentation for the ClientOptions type in the mongo/options package. type Client struct { - id uuid.UUID - deployment driver.Deployment - localThreshold time.Duration - retryWrites bool - retryReads bool - clock *session.ClusterClock - readPreference *readpref.ReadPref - readConcern *readconcern.ReadConcern - writeConcern *writeconcern.WriteConcern - bsonOpts *options.BSONOptions - registry *bson.Registry - monitor *event.CommandMonitor - serverAPI *driver.ServerAPIOptions - serverMonitor *event.ServerMonitor - sessionPool *session.Pool - timeout *time.Duration - httpClient *http.Client - logger *logger.Logger + id uuid.UUID + deployment driver.Deployment + localThreshold time.Duration + retryWrites bool + retryReads bool + clock *session.ClusterClock + readPreference *readpref.ReadPref + readConcern *readconcern.ReadConcern + writeConcern *writeconcern.WriteConcern + bsonOpts *options.BSONOptions + registry *bson.Registry + monitor *event.CommandMonitor + serverAPI *driver.ServerAPIOptions + serverMonitor *event.ServerMonitor + sessionPool *session.Pool + timeout *time.Duration + httpClient *http.Client + logger *logger.Logger + currentDriverInfo *atomic.Pointer[options.DriverInfo] // in-use encryption fields isAutoEncryptionSet bool @@ -132,7 +134,11 @@ func newClient(opts ...*options.ClientOptions) (*Client, error) { if err != nil { return nil, err } - client := &Client{id: id} + + client := &Client{ + id: id, + currentDriverInfo: &atomic.Pointer[options.DriverInfo]{}, + } // ClusterClock client.clock = new(session.ClusterClock) @@ -217,7 +223,16 @@ func newClient(opts ...*options.ClientOptions) (*Client, error) { } } - cfg, err := topology.NewConfigFromOptionsWithAuthenticator(clientOpts, client.clock, client.authenticator) + if clientOpts.DriverInfo != nil { + client.AppendDriverInfo(*clientOpts.DriverInfo) + } + + cfg, err := topology.NewAuthenticatorConfig(client.authenticator, + topology.WithAuthConfigClock(client.clock), + topology.WithAuthConfigClientOptions(clientOpts), + topology.WithAuthConfigDriverInfo(client.currentDriverInfo), + ) + if err != nil { return nil, err } @@ -294,6 +309,51 @@ func (c *Client) connect() error { return nil } +// AppendDriverInfo appends the provided DriverInfo to the driver information +// that will be sent to the server in handshake requests when establishing new +// connections. The provided info will overwrite any existing values. +// +// AppendsDriverInfo appends the provided [options.DriverInfo] to the metadata +// (e.g. name, version, platform) that will be sent to the server in handshake +// requests when establishing new connections. The provided info will overwrite +// any existing values. +// +// Metadata is limited to 512 bytes; any excess will be truncated. +// +// TODO: Should this send a sentinel error if appending is rejectd? It could +// require blocking, which is annoying. +func (c *Client) AppendDriverInfo(info options.DriverInfo) { + if c == nil { + return + } + + if old := c.currentDriverInfo.Load(); old != nil { + if old.Name != "" && info.Name != "" && old.Name != info.Name { + info.Name = old.Name + "|" + info.Name + } else if old.Name != "" { + info.Name = old.Name + } + + if old.Version != "" && info.Version != "" && old.Version != info.Version { + info.Version = old.Version + "|" + info.Version + } else if old.Version != "" { + info.Version = old.Version + } + + if old.Platform != "" && info.Platform != "" && old.Platform != info.Platform { + info.Platform = old.Platform + "|" + info.Platform + } else if old.Platform != "" { + info.Platform = old.Platform + } + } + + // Copy-on-write so that the info stored in the client is immutable. + copy := new(options.DriverInfo) + *copy = info + + c.currentDriverInfo.Store(copy) +} + // Disconnect closes sockets to the topology referenced by this Client. It will // shut down any monitoring goroutines, close the idle connection pool, and will // wait until all the in use connections have been returned to the connection diff --git a/x/mongo/driver/topology/server.go b/x/mongo/driver/topology/server.go index b665387404..682fba0f73 100644 --- a/x/mongo/driver/topology/server.go +++ b/x/mongo/driver/topology/server.go @@ -807,9 +807,16 @@ func (s *Server) createConnection() *connection { opts := copyConnectionOpts(s.cfg.connectionOpts) opts = append(opts, WithHandshaker(func(Handshaker) Handshaker { - return operation.NewHello().AppName(s.cfg.appname).Compressors(s.cfg.compressionOpts). - ServerAPI(s.cfg.serverAPI).OuterLibraryName(s.cfg.outerLibraryName). - OuterLibraryVersion(s.cfg.outerLibraryVersion).OuterLibraryPlatform(s.cfg.outerLibraryPlatform) + handshaker := operation.NewHello().AppName(s.cfg.appname).Compressors(s.cfg.compressionOpts). + ServerAPI(s.cfg.serverAPI) + + if s.cfg.driverInfo != nil { + driverInfo := s.cfg.driverInfo.Load() + handshaker = handshaker.OuterLibraryName(driverInfo.Name).OuterLibraryVersion(driverInfo.Version). + OuterLibraryPlatform(driverInfo.Platform) + } + + return handshaker }), // Override any monitors specified in options with nil to avoid monitoring heartbeats. WithMonitor(func(*event.CommandMonitor) *event.CommandMonitor { return nil }), diff --git a/x/mongo/driver/topology/server_options.go b/x/mongo/driver/topology/server_options.go index 490834cbef..69e7b31c91 100644 --- a/x/mongo/driver/topology/server_options.go +++ b/x/mongo/driver/topology/server_options.go @@ -7,11 +7,13 @@ package topology import ( + "sync/atomic" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/internal/logger" + "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/x/mongo/driver" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/connstring" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/session" @@ -32,6 +34,7 @@ type serverConfig struct { monitoringDisabled bool serverAPI *driver.ServerAPIOptions loadBalanced bool + driverInfo *atomic.Pointer[options.DriverInfo] // Connection pool options. maxConns uint64 @@ -41,11 +44,6 @@ type serverConfig struct { logger *logger.Logger poolMaxIdleTime time.Duration poolMaintainInterval time.Duration - - // Fields provided by a library that wraps the Go Driver. - outerLibraryName string - outerLibraryVersion string - outerLibraryPlatform string } func newServerConfig(connectTimeout time.Duration, opts ...ServerOption) *serverConfig { @@ -101,27 +99,12 @@ func WithServerAppName(fn func(string) string) ServerOption { } } -// WithOuterLibraryName configures the name for the outer library to include -// in the drivers section of the handshake metadata. -func WithOuterLibraryName(fn func(string) string) ServerOption { - return func(cfg *serverConfig) { - cfg.outerLibraryName = fn(cfg.outerLibraryName) - } -} - -// WithOuterLibraryVersion configures the version for the outer library to -// include in the drivers section of the handshake metadata. -func WithOuterLibraryVersion(fn func(string) string) ServerOption { - return func(cfg *serverConfig) { - cfg.outerLibraryVersion = fn(cfg.outerLibraryVersion) - } -} - -// WithOuterLibraryPlatform configures the platform for the outer library to -// include in the platform section of the handshake metadata. -func WithOuterLibraryPlatform(fn func(string) string) ServerOption { +// WithDriverInfo sets at atomic pointer to the server configuration, which will +// be used to create the "driver" section on handshake commands. An atomic +// pointer is used so that the driver info can be updated concurrently. +func WithDriverInfo(into *atomic.Pointer[options.DriverInfo]) ServerOption { return func(cfg *serverConfig) { - cfg.outerLibraryPlatform = fn(cfg.outerLibraryPlatform) + cfg.driverInfo = into } } diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 2ddc7434bd..aee32a141c 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -11,6 +11,7 @@ import ( "crypto/tls" "fmt" "net/http" + "sync/atomic" "time" "go.mongodb.org/mongo-driver/v2/event" @@ -139,14 +140,51 @@ func NewConfig(opts *options.ClientOptions, clock *session.ClusterClock) (*Confi return nil, fmt.Errorf("error creating authenticator: %w", err) } } - return NewConfigFromOptionsWithAuthenticator(opts, clock, authenticator) + return NewAuthenticatorConfig(authenticator, + WithAuthConfigClock(clock), + WithAuthConfigClientOptions(opts), + ) +} + +type authConfigOptions struct { + clock *session.ClusterClock + opts *options.ClientOptions + driverInfo *atomic.Pointer[options.DriverInfo] +} + +type AuthConfigOption func(*authConfigOptions) + +func WithAuthConfigClock(clock *session.ClusterClock) AuthConfigOption { + return func(co *authConfigOptions) { + co.clock = clock + } +} + +func WithAuthConfigClientOptions(opts *options.ClientOptions) AuthConfigOption { + return func(co *authConfigOptions) { + co.opts = opts + } +} + +func WithAuthConfigDriverInfo(driverInfo *atomic.Pointer[options.DriverInfo]) AuthConfigOption { + return func(co *authConfigOptions) { + co.driverInfo = driverInfo + } } -// NewConfigFromOptionsWithAuthenticator will translate data from client options into a +// NewAuthenticatorConfig will translate data from client options into a // topology config for building non-default deployments. Server and topology // options are not honored if a custom deployment is used. It uses a passed in // authenticator to authenticate the connection. -func NewConfigFromOptionsWithAuthenticator(opts *options.ClientOptions, clock *session.ClusterClock, authenticator driver.Authenticator) (*Config, error) { +func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...AuthConfigOption) (*Config, error) { + settings := authConfigOptions{} + for _, apply := range clientOpts { + apply(&settings) + } + + opts := settings.opts + clock := settings.clock + var serverAPI *driver.ServerAPIOptions if err := opts.Validate(); err != nil { @@ -200,23 +238,8 @@ func NewConfigFromOptionsWithAuthenticator(opts *options.ClientOptions, clock *s })) } - var outerLibraryName, outerLibraryVersion, outerLibraryPlatform string if opts.DriverInfo != nil { - outerLibraryName = opts.DriverInfo.Name - outerLibraryVersion = opts.DriverInfo.Version - outerLibraryPlatform = opts.DriverInfo.Platform - - serverOpts = append(serverOpts, WithOuterLibraryName(func(string) string { - return outerLibraryName - })) - - serverOpts = append(serverOpts, WithOuterLibraryVersion(func(string) string { - return outerLibraryVersion - })) - - serverOpts = append(serverOpts, WithOuterLibraryPlatform(func(string) string { - return outerLibraryPlatform - })) + serverOpts = append(serverOpts, WithDriverInfo(settings.driverInfo)) } // Compressors & ZlibLevel @@ -257,15 +280,18 @@ func NewConfigFromOptionsWithAuthenticator(opts *options.ClientOptions, clock *s var handshaker func(driver.Handshaker) driver.Handshaker if authenticator != nil { handshakeOpts := &auth.HandshakeOptions{ - AppName: appName, - Authenticator: authenticator, - Compressors: comps, - ServerAPI: serverAPI, - LoadBalanced: loadBalanced, - ClusterClock: clock, - OuterLibraryName: outerLibraryName, - OuterLibraryVersion: outerLibraryVersion, - OuterLibraryPlatform: outerLibraryPlatform, + AppName: appName, + Authenticator: authenticator, + Compressors: comps, + ServerAPI: serverAPI, + LoadBalanced: loadBalanced, + ClusterClock: clock, + } + + if driverInfo := settings.driverInfo; driverInfo != nil && driverInfo.Load() != nil { + handshakeOpts.OuterLibraryName = driverInfo.Load().Name + handshakeOpts.OuterLibraryVersion = driverInfo.Load().Version + handshakeOpts.OuterLibraryPlatform = driverInfo.Load().Platform } if opts.Auth.AuthMechanism == "" { @@ -287,6 +313,13 @@ func NewConfigFromOptionsWithAuthenticator(opts *options.ClientOptions, clock *s } else { handshaker = func(driver.Handshaker) driver.Handshaker { + var outerLibraryName, outerLibraryVersion, outerLibraryPlatform string + if driverInfo := settings.driverInfo; driverInfo != nil && driverInfo.Load() != nil { + outerLibraryName = driverInfo.Load().Name + outerLibraryVersion = driverInfo.Load().Version + outerLibraryPlatform = driverInfo.Load().Platform + } + return operation.NewHello(). AppName(appName). Compressors(comps). diff --git a/x/mongo/driver/topology/topology_options_test.go b/x/mongo/driver/topology/topology_options_test.go index 680aa638a7..9cdd4da8b2 100644 --- a/x/mongo/driver/topology/topology_options_test.go +++ b/x/mongo/driver/topology/topology_options_test.go @@ -149,7 +149,7 @@ func TestAuthenticateToAnything(t *testing.T) { opt := options.Client().SetAuth(options.Credential{Username: "foo", Password: "bar"}) err := tc.set(opt) require.NoError(t, err, "error setting authenticateToAnything: %v", err) - cfg, err := NewConfigFromOptionsWithAuthenticator(opt, nil, &testAuthenticator{}) + cfg, err := NewAuthenticatorConfig(opt, nil, &testAuthenticator{}) require.NoError(t, err, "error constructing topology config: %v", err) srvrCfg := newServerConfig(defaultConnectionTimeout, cfg.ServerOpts...) From 751104f19bf39752f2413cb675e6a573981d878e Mon Sep 17 00:00:00 2001 From: Preston Vasquez Date: Thu, 11 Sep 2025 22:57:44 -0600 Subject: [PATCH 02/13] Finish prose tests --- .../assertbsoncore/assertions_bsoncore.go | 35 +- internal/handshake/handshake.go | 21 ++ internal/integration/client_test.go | 2 +- internal/integration/handshake_test.go | 354 +++++++++++++++++- internal/integration/mtest/mongotest.go | 37 +- internal/integration/mtest/proxy_dialer.go | 1 - .../unified/client_operation_execution.go | 3 +- internal/test/client_metadata.go | 7 - mongo/client.go | 15 +- x/mongo/driver/topology/server.go | 6 +- x/mongo/driver/topology/server_options.go | 4 +- x/mongo/driver/topology/topology_options.go | 6 +- .../driver/topology/topology_options_test.go | 2 +- 13 files changed, 433 insertions(+), 60 deletions(-) diff --git a/internal/assert/assertbsoncore/assertions_bsoncore.go b/internal/assert/assertbsoncore/assertions_bsoncore.go index 2a47a243b5..872192d922 100644 --- a/internal/assert/assertbsoncore/assertions_bsoncore.go +++ b/internal/assert/assertbsoncore/assertions_bsoncore.go @@ -7,26 +7,41 @@ package assertbsoncore import ( + "errors" "testing" "go.mongodb.org/mongo-driver/v2/internal/assert" + "go.mongodb.org/mongo-driver/v2/internal/handshake" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) +// HandshakeClientMetadata compares the client metadata in two wire messages. It +// extracts the client metadata document from each wire message and compares +// them. If the document is not found, it assumes the wire message is just the +// value of the client metadata document itself. func HandshakeClientMetadata(t testing.TB, expectedWM, actualWM []byte) bool { - command := bsoncore.Document(actualWM) - - // Lookup the "client" field in the command document. - clientVal, err := command.LookupErr("client") + gotCommand, err := handshake.ParseClientMetadata(actualWM) if err != nil { - return assert.Fail(t, "expected command to contain the 'client' field") + if errors.Is(err, bsoncore.ErrElementNotFound) { + // If the element is not found, the actual wire message may just be the + // client metadata document itself. + gotCommand = bsoncore.Document(actualWM) + } else { + return assert.Fail(t, "error parsing actual wire message: %v", err) + } } - GotCommand, ok := clientVal.DocumentOK() - if !ok { - return assert.Fail(t, "expected client field to be a document, got %s", clientVal.Type) + wantCommand, err := handshake.ParseClientMetadata(expectedWM) + if err != nil { + // If the element is not found, the expected wire message may just be the + // client metadata document itself. + if errors.Is(err, bsoncore.ErrElementNotFound) { + wantCommand = bsoncore.Document(expectedWM) + } else { + return assert.Fail(t, "error parsing expected wire message: %v", err) + } } - wantCommand := bsoncore.Document(expectedWM) - return assert.Equal(t, wantCommand, GotCommand, "want: %v, got: %v", wantCommand, GotCommand) + return assert.Equal(t, wantCommand, gotCommand, + "expected: %v, got: %v", bsoncore.Document(wantCommand), bsoncore.Document(gotCommand)) } diff --git a/internal/handshake/handshake.go b/internal/handshake/handshake.go index c9537d3ef8..f66fd8d34f 100644 --- a/internal/handshake/handshake.go +++ b/internal/handshake/handshake.go @@ -6,8 +6,29 @@ package handshake +import ( + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" +) + // LegacyHello is the legacy version of the hello command. var LegacyHello = "isMaster" // LegacyHelloLowercase is the lowercase, legacy version of the hello command. var LegacyHelloLowercase = "ismaster" + +func ParseClientMetadata(msg []byte) ([]byte, error) { + command := bsoncore.Document(msg) + + // Lookup the "client" field in the command document. + clientMetadataRaw, err := command.LookupErr("client") + if err != nil { + return nil, err + } + + clientMetadata, ok := clientMetadataRaw.DocumentOK() + if !ok { + return nil, err + } + + return clientMetadata, nil +} diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 0bf0e3ecd6..fca8e059c3 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -457,7 +457,7 @@ func TestClient(t *testing.T) { err := mt.Client.Ping(context.Background(), mtest.PrimaryRp) assert.Nil(mt, err, "Ping error: %v", err) - want := test.EncodeClientMetadata(mt, test.WithClientMentadataAppName("foo")) + want := test.EncodeClientMetadata(mt, test.WithClientMetadataAppName("foo")) for i := 0; i < 2; i++ { message := mt.GetProxyCapture().TryNext() require.NotNil(mt, message, "expected handshake message, got nil") diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index 628443d331..65211ed9a8 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -19,7 +19,6 @@ import ( "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/internal/test" "go.mongodb.org/mongo-driver/v2/mongo/options" - "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/wiremessage" ) @@ -528,8 +527,8 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { } } -// Test 4: Multiple Metadata Updates with Duplicate Dataa -func TestHandshakeProse_AppendMetadata_Test4(t *testing.T) { +// 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) @@ -597,9 +596,350 @@ func TestHandshakeProse_AppendMetadata_Test4(t *testing.T) { require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - // 12. Assert that `clientMetadata` is identical to `updatedClientMetadata`. - want := bsoncore.Document(clientMetadata.Sent.Command) - got := bsoncore.Document(updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) +} + +// 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) + + clientMetadata := mt.GetProxyCapture().TryNext() + require.NotNil(mt, clientMetadata, "expected to capture a proxied message") + assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + + // 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") + + assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) +} + +// 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) - assert.Equal(mt, want, got, "expected: %s, got: %s", want, got) + // 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`. + clientMetadata := mt.GetProxyCapture().TryNext() + require.NotNil(mt, clientMetadata, "expected to capture a proxied message") + assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + + // 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") + + assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) +} + +// 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 + }{ + { + name: "name empty", + initialDriverInfo: options.DriverInfo{ + Name: "", + Version: "1.2", + Platform: "Library Platform", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "", + Version: "1.2", + Platform: "Library Platform", + }, + }, + { + name: "version empty", + initialDriverInfo: options.DriverInfo{ + Name: "library", + Version: "", + Platform: "Library Platform", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "library", + Version: "", + Platform: "Library Platform", + }, + }, + { + name: "platform empty", + initialDriverInfo: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "", + }, + }, + } + + for _, tc := range testCases { + // 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`. + 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") + + // 5. Wait 5ms for the connection to become idle. + time.Sleep(20 * 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") + + assertbsoncore.HandshakeClientMetadata(mt, initialClientMetadata.Sent.Command, + updatedClientMetadata.Sent.Command) + }) + } +} + +// 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 + }{ + { + name: "name empty", + initialDriverInfo: options.DriverInfo{ + Name: "", + Version: "1.2", + Platform: "Library Platform", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "", + Version: "1.2", + Platform: "Library Platform", + }, + }, + { + name: "version empty", + initialDriverInfo: options.DriverInfo{ + Name: "library", + Version: "", + Platform: "Library Platform", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "library", + Version: "", + Platform: "Library Platform", + }, + }, + { + name: "platform empty", + initialDriverInfo: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "", + }, + toAppendDriverInfo: options.DriverInfo{ + Name: "library", + Version: "1.2", + Platform: "", + }, + }, + } + + 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`. + 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") + + // 4. Wait 5ms for the connection to become idle. + time.Sleep(20 * 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`. + assertbsoncore.HandshakeClientMetadata(mt, initialClientMetadata.Sent.Command, + updatedClientMetadata.Sent.Command) + }) + } } diff --git a/internal/integration/mtest/mongotest.go b/internal/integration/mtest/mongotest.go index 2afdd6f4f1..db8c664a41 100644 --- a/internal/integration/mtest/mongotest.go +++ b/internal/integration/mtest/mongotest.go @@ -60,25 +60,24 @@ type T struct { *testing.T // members for only this T instance - createClient *bool - createCollection *bool - runOn []RunOnBlock - mockDeployment *drivertest.MockDeployment // nil if the test is not being run against a mock - mockResponses []bson.D - createdColls []*Collection // collections created in this test - proxyDialer *proxyDialer - proxyDialerHandshakeQueue <-chan *ProxyMessage // FIFO queue of handshake messages sent by the proxy dialer - dbName, collName string - failPointNames []string - minServerVersion string - maxServerVersion string - validTopologies []TopologyKind - auth *bool - enterprise *bool - dataLake *bool - ssl *bool - collCreateOpts *options.CreateCollectionOptionsBuilder - requireAPIVersion *bool + createClient *bool + createCollection *bool + runOn []RunOnBlock + mockDeployment *drivertest.MockDeployment // nil if the test is not being run against a mock + mockResponses []bson.D + createdColls []*Collection // collections created in this test + proxyDialer *proxyDialer + dbName, collName string + failPointNames []string + minServerVersion string + maxServerVersion string + validTopologies []TopologyKind + auth *bool + enterprise *bool + dataLake *bool + ssl *bool + collCreateOpts *options.CreateCollectionOptionsBuilder + requireAPIVersion *bool // options copied to sub-tests clientType ClientType diff --git a/internal/integration/mtest/proxy_dialer.go b/internal/integration/mtest/proxy_dialer.go index 8e750500eb..0d980c406c 100644 --- a/internal/integration/mtest/proxy_dialer.go +++ b/internal/integration/mtest/proxy_dialer.go @@ -34,7 +34,6 @@ type proxyDialer struct { *net.Dialer sync.Mutex - //messages []*ProxyMessage // sentMap temporarily stores the message sent to the server using the requestID so it can map requests to their // responses. sentMap sync.Map diff --git a/internal/integration/unified/client_operation_execution.go b/internal/integration/unified/client_operation_execution.go index 25d42743e6..1631d88ba7 100644 --- a/internal/integration/unified/client_operation_execution.go +++ b/internal/integration/unified/client_operation_execution.go @@ -323,8 +323,7 @@ func executeAppendMetadata(ctx context.Context, op *operation) (*operationResult key := elem.Key() val := elem.Value() - switch key { - case "driverInfoOptions": + if key == "driverInfoOptions" { if err = bson.Unmarshal(val.Value, &driverInfo); err != nil { return nil, fmt.Errorf("error unmarshaling driverInfoOptions: %w", err) } diff --git a/internal/test/client_metadata.go b/internal/test/client_metadata.go index a35c8fe5a4..fb70977a65 100644 --- a/internal/test/client_metadata.go +++ b/internal/test/client_metadata.go @@ -30,13 +30,6 @@ type clientMetadataOptions struct { // metadata. type ClientMetadataOption func(*clientMetadataOptions) -// Existing (note the typo in the function name). Kept for compatibility. -func WithClientMentadataAppName(name string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.appName = name - } -} - // WithClientMetadataAppName sets the application name included in client metadata. func WithClientMetadataAppName(name string) ClientMetadataOption { return func(o *clientMetadataOptions) { diff --git a/mongo/client.go b/mongo/client.go index 4c781fbe55..3f5b278321 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net/http" + "sync" "sync/atomic" "time" @@ -76,6 +77,7 @@ type Client struct { httpClient *http.Client logger *logger.Logger currentDriverInfo *atomic.Pointer[options.DriverInfo] + seenDriverInfo sync.Map // in-use encryption fields isAutoEncryptionSet bool @@ -318,12 +320,11 @@ func (c *Client) connect() error { // requests when establishing new connections. The provided info will overwrite // any existing values. // -// Metadata is limited to 512 bytes; any excess will be truncated. +// Repeated calls to appendMetadata with equivalent DriverInfo is a no-op. // -// TODO: Should this send a sentinel error if appending is rejectd? It could -// require blocking, which is annoying. +// Metadata is limited to 512 bytes; any excess will be truncated. func (c *Client) AppendDriverInfo(info options.DriverInfo) { - if c == nil { + if _, loaded := c.seenDriverInfo.LoadOrStore(info, struct{}{}); loaded { return } @@ -348,10 +349,10 @@ func (c *Client) AppendDriverInfo(info options.DriverInfo) { } // Copy-on-write so that the info stored in the client is immutable. - copy := new(options.DriverInfo) - *copy = info + infoCopy := new(options.DriverInfo) + *infoCopy = info - c.currentDriverInfo.Store(copy) + c.currentDriverInfo.Store(infoCopy) } // Disconnect closes sockets to the topology referenced by this Client. It will diff --git a/x/mongo/driver/topology/server.go b/x/mongo/driver/topology/server.go index 682fba0f73..a408aee561 100644 --- a/x/mongo/driver/topology/server.go +++ b/x/mongo/driver/topology/server.go @@ -812,8 +812,10 @@ func (s *Server) createConnection() *connection { if s.cfg.driverInfo != nil { driverInfo := s.cfg.driverInfo.Load() - handshaker = handshaker.OuterLibraryName(driverInfo.Name).OuterLibraryVersion(driverInfo.Version). - OuterLibraryPlatform(driverInfo.Platform) + if driverInfo != nil { + handshaker = handshaker.OuterLibraryName(driverInfo.Name).OuterLibraryVersion(driverInfo.Version). + OuterLibraryPlatform(driverInfo.Platform) + } } return handshaker diff --git a/x/mongo/driver/topology/server_options.go b/x/mongo/driver/topology/server_options.go index 69e7b31c91..297cafc701 100644 --- a/x/mongo/driver/topology/server_options.go +++ b/x/mongo/driver/topology/server_options.go @@ -102,9 +102,9 @@ func WithServerAppName(fn func(string) string) ServerOption { // WithDriverInfo sets at atomic pointer to the server configuration, which will // be used to create the "driver" section on handshake commands. An atomic // pointer is used so that the driver info can be updated concurrently. -func WithDriverInfo(into *atomic.Pointer[options.DriverInfo]) ServerOption { +func WithDriverInfo(info *atomic.Pointer[options.DriverInfo]) ServerOption { return func(cfg *serverConfig) { - cfg.driverInfo = into + cfg.driverInfo = info } } diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index aee32a141c..2e46d2c7ef 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -152,20 +152,24 @@ type authConfigOptions struct { driverInfo *atomic.Pointer[options.DriverInfo] } +// AuthConfigOption is a function that configures authConfigOptions. type AuthConfigOption func(*authConfigOptions) +// WithAuthConfigClock sets the cluster clock in authConfigOptions. func WithAuthConfigClock(clock *session.ClusterClock) AuthConfigOption { return func(co *authConfigOptions) { co.clock = clock } } +// WithAuthConfigClientOptions sets the client options in authConfigOptions. func WithAuthConfigClientOptions(opts *options.ClientOptions) AuthConfigOption { return func(co *authConfigOptions) { co.opts = opts } } +// WithAuthConfigDriverInfo sets the driver info in authConfigOptions. func WithAuthConfigDriverInfo(driverInfo *atomic.Pointer[options.DriverInfo]) AuthConfigOption { return func(co *authConfigOptions) { co.driverInfo = driverInfo @@ -238,7 +242,7 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au })) } - if opts.DriverInfo != nil { + if settings.driverInfo != nil { serverOpts = append(serverOpts, WithDriverInfo(settings.driverInfo)) } diff --git a/x/mongo/driver/topology/topology_options_test.go b/x/mongo/driver/topology/topology_options_test.go index 9cdd4da8b2..402503b300 100644 --- a/x/mongo/driver/topology/topology_options_test.go +++ b/x/mongo/driver/topology/topology_options_test.go @@ -149,7 +149,7 @@ func TestAuthenticateToAnything(t *testing.T) { opt := options.Client().SetAuth(options.Credential{Username: "foo", Password: "bar"}) err := tc.set(opt) require.NoError(t, err, "error setting authenticateToAnything: %v", err) - cfg, err := NewAuthenticatorConfig(opt, nil, &testAuthenticator{}) + cfg, err := NewAuthenticatorConfig(nil, WithAuthConfigClientOptions(opt)) require.NoError(t, err, "error constructing topology config: %v", err) srvrCfg := newServerConfig(defaultConnectionTimeout, cfg.ServerOpts...) From 9373c483e196528dd78d61b4a7689f32a3c9908b Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:21:08 -0700 Subject: [PATCH 03/13] Use BSON document literals to define the expected metadata documents. --- internal/integration/client_test.go | 19 +- internal/integration/handshake_test.go | 418 ++++++++++++++++++------- internal/test/client_metadata.go | 199 ------------ 3 files changed, 319 insertions(+), 317 deletions(-) delete mode 100644 internal/test/client_metadata.go diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index fca8e059c3..cc6d53933c 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -12,6 +12,7 @@ import ( "net" "os" "reflect" + "runtime" "strings" "testing" "time" @@ -25,11 +26,11 @@ import ( "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" "go.mongodb.org/mongo-driver/v2/internal/integtest" "go.mongodb.org/mongo-driver/v2/internal/require" - "go.mongodb.org/mongo-driver/v2/internal/test" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/mongo/readpref" "go.mongodb.org/mongo-driver/v2/mongo/writeconcern" + "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" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/wiremessage" @@ -457,7 +458,21 @@ func TestClient(t *testing.T) { err := mt.Client.Ping(context.Background(), mtest.PrimaryRp) assert.Nil(mt, err, "Ping error: %v", err) - want := test.EncodeClientMetadata(mt, test.WithClientMetadataAppName("foo")) + 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: "application", Value: bson.D{ + bson.E{Key: "name", Value: "foo"}, + }}, + }) + for i := 0; i < 2; i++ { message := mt.GetProxyCapture().TryNext() require.NotNil(mt, message, "expected handshake message, got nil") diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index 65211ed9a8..c9e4c1f725 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -9,16 +9,17 @@ 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/assertbsoncore" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" - "go.mongodb.org/mongo-driver/v2/internal/ptrutil" "go.mongodb.org/mongo-driver/v2/internal/require" - "go.mongodb.org/mongo-driver/v2/internal/test" "go.mongodb.org/mongo-driver/v2/mongo/options" + "go.mongodb.org/mongo-driver/v2/version" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/wiremessage" ) @@ -66,11 +67,22 @@ func TestHandshakeProse(t *testing.T) { "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("aws.lambda"), - test.WithClientMetadataEnvMemoryMB(ptrutil.Ptr(1024)), - test.WithClientMetadataEnvRegion("us-east-2"), - ), + 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", @@ -78,9 +90,20 @@ func TestHandshakeProse(t *testing.T) { "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("azure.func"), - ), + 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", @@ -91,12 +114,23 @@ func TestHandshakeProse(t *testing.T) { "FUNCTION_REGION": "us-central1", }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("gcp.func"), - test.WithClientMetadataEnvMemoryMB(ptrutil.Ptr(1024)), - test.WithClientMetadataEnvRegion("us-central1"), - test.WithClientMetadataEnvTimeoutSec(ptrutil.Ptr(60)), - ), + 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", @@ -105,10 +139,21 @@ func TestHandshakeProse(t *testing.T) { "VERCEL_REGION": "cdg1", }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("vercel"), - test.WithClientMetadataEnvRegion("cdg1"), - ), + 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", @@ -117,7 +162,17 @@ func TestHandshakeProse(t *testing.T) { "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, - want: test.EncodeClientMetadata(mt), + 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", @@ -132,9 +187,20 @@ func TestHandshakeProse(t *testing.T) { }(), }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("aws.lambda"), - ), + 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", @@ -143,9 +209,20 @@ func TestHandshakeProse(t *testing.T) { "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big", }, opts: nil, - want: test.EncodeClientMetadata(mt, - test.WithClientMetadataEnvName("aws.lambda"), - ), + 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_\"", @@ -153,16 +230,32 @@ func TestHandshakeProse(t *testing.T) { "AWS_EXECUTION_ENV": "EC2", }, opts: nil, - want: test.EncodeClientMetadata(mt), + 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: test.EncodeClientMetadata(mt, - test.WithClientMetadataDriverName("outer-library-name"), - test.WithClientMetadataDriverVersion("outer-library-version"), - test.WithClientMetadataDriverPlatform("outer-library-platform"), - ), + 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"}, + }), }, } @@ -250,7 +343,7 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { testCases := []struct { name string driverInfo options.DriverInfo - want options.DriverInfo + want []byte // append initialDriverInfo using client.AppendDriverInfo instead of as a // client-level constructor. @@ -263,11 +356,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2|2.0", - Platform: "Library 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, }, { @@ -277,11 +376,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2|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: false, }, { @@ -291,11 +396,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2", - Platform: "Library 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, }, { @@ -305,11 +416,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "", Platform: "", }, - want: options.DriverInfo{ - Name: "library|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: false, }, { @@ -319,11 +436,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2|2.0", - Platform: "Library 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, }, { @@ -333,11 +456,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2|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, }, { @@ -347,11 +476,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2", - Platform: "Library 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, }, { @@ -361,11 +496,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "", Platform: "", }, - want: options.DriverInfo{ - Name: "library|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, }, { @@ -375,11 +516,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "1.2", Platform: "Library Platform", }, - want: 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, }, { @@ -389,11 +536,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "1.2", Platform: "Library Platform", }, - want: options.DriverInfo{ - Name: "library|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, }, { @@ -403,11 +556,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "Library Platform", }, - want: options.DriverInfo{ - Name: "library", - Version: "1.2|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, }, { @@ -417,11 +576,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "1.2", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library", - Version: "1.2", - Platform: "Library 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, }, { @@ -431,11 +596,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "Library Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2|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, }, { @@ -445,11 +616,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "1.2", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library|framework", - Version: "1.2", - Platform: "Library 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, }, { @@ -459,11 +636,17 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { Version: "2.0", Platform: "Framework Platform", }, - want: options.DriverInfo{ - Name: "library", - Version: "1.2|2.0", - Platform: "Library 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, }, } @@ -516,13 +699,7 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { require.NotNil(mt, gotMessage, "expected to capture a proxied message") assert.True(mt, gotMessage.IsHandshake(), "expected first message to be a handshake") - want := test.EncodeClientMetadata(mt, - test.WithClientMetadataDriverName(tc.want.Name), - test.WithClientMetadataDriverVersion(tc.want.Version), - test.WithClientMetadataDriverPlatform(tc.want.Platform), - ) - - assertbsoncore.HandshakeClientMetadata(mt, want, gotMessage.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, tc.want, gotMessage.Sent.Command) }) } } @@ -943,3 +1120,12 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing }) } } + +func mustMarshalBSON(val interface{}) []byte { + bytes, err := bson.Marshal(val) + if err != nil { + panic(err) + } + + return bytes +} diff --git a/internal/test/client_metadata.go b/internal/test/client_metadata.go deleted file mode 100644 index fb70977a65..0000000000 --- a/internal/test/client_metadata.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (C) MongoDB, Inc. 2025-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 test - -import ( - "runtime" - "testing" - - "go.mongodb.org/mongo-driver/v2/bson" - "go.mongodb.org/mongo-driver/v2/internal/require" - "go.mongodb.org/mongo-driver/v2/version" -) - -type clientMetadataOptions struct { - appName string - driverName string - driverVersion string - driverPlatform string - envName string - envTimeoutSec *int - envMemoryMB *int - envRegion string -} - -// ClientMetadataOption represents a configuration option for building client -// metadata. -type ClientMetadataOption func(*clientMetadataOptions) - -// WithClientMetadataAppName sets the application name included in client metadata. -func WithClientMetadataAppName(name string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.appName = name - } -} - -// WithClientMetadataDriverName sets the driver name (e.g., "mongo-go-driver"). -func WithClientMetadataDriverName(name string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.driverName = name - } -} - -// WithClientMetadataDriverVersion sets the driver version (e.g., "1.16.0"). -func WithClientMetadataDriverVersion(version string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.driverVersion = version - } -} - -// WithClientMetadataDriverPlatform sets the driver platform string -// (e.g., "go1.22.5 gc linux/amd64"). -func WithClientMetadataDriverPlatform(platform string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.driverPlatform = platform - } -} - -// WithClientMetadataEnvName sets the execution environment name -// (e.g., "AWS Lambda", "GCP Cloud Functions", "Kubernetes"). -func WithClientMetadataEnvName(name string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.envName = name - } -} - -// WithClientMetadataEnvTimeoutSec sets the execution timeout in seconds. -// Pass nil to indicate "unspecified" or "not applicable". -func WithClientMetadataEnvTimeoutSec(timeoutSec *int) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.envTimeoutSec = timeoutSec - } -} - -// WithClientMetadataEnvMemoryMB sets the memory limit in megabytes. -// Pass nil to indicate "unspecified" or "not applicable". -func WithClientMetadataEnvMemoryMB(memoryMB *int) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.envMemoryMB = memoryMB - } -} - -// WithClientMetadataEnvRegion sets the deployment/region identifier -// (e.g., "us-east-1", "europe-west1"). -func WithClientMetadataEnvRegion(region string) ClientMetadataOption { - return func(o *clientMetadataOptions) { - o.envRegion = region - } -} - -// EncodeClientMetadata constructs the WM byte slice that represents the client -// metadata document for the given options with the intent of comparing to an -// actual handshake wire message: -// -// { -// application: { -// name: "" -// }, -// driver: { -// name: "", -// version: "" -// }, -// platform: "", -// os: { -// type: "", -// name: "", -// architecture: "", -// version: "" -// }, -// env: { -// name: "", -// timeout_sec: 42, -// memory_mb: 1024, -// region: "", -// container: { -// runtime: "", -// orchestrator: "" -// } -// } -// } -// -// This function was not put in mtest since it could be used in non-integration -// test conditions. -func EncodeClientMetadata(t testing.TB, opts ...ClientMetadataOption) []byte { - t.Helper() - - cfg := clientMetadataOptions{} - for _, apply := range opts { - apply(&cfg) - } - - var ( - driverName = "mongo-go-driver" // Default - driverVersion = version.Driver - platform = runtime.Version() // Default - ) - - if cfg.driverName != "" { - driverName = driverName + "|" + cfg.driverName - } - - if cfg.driverVersion != "" { - driverVersion = driverVersion + "|" + cfg.driverVersion - } - - if cfg.driverPlatform != "" { - platform = platform + "|" + cfg.driverPlatform - } - - elems := bson.D{} - - if cfg.appName != "" { - elems = append(elems, bson.E{Key: "application", Value: bson.D{ - {Key: "name", Value: cfg.appName}, - }}) - } - - elems = append(elems, bson.D{ - {Key: "driver", Value: bson.D{ - {Key: "name", Value: driverName}, - {Key: "version", Value: driverVersion}, - }}, - {Key: "os", Value: bson.D{ - {Key: "type", Value: runtime.GOOS}, - {Key: "architecture", Value: runtime.GOARCH}, - }}, - }...) - - elems = append(elems, bson.E{Key: "platform", Value: platform}) - - envElems := bson.D{} - if cfg.envName != "" { - envElems = append(envElems, bson.E{Key: "name", Value: cfg.envName}) - } - - if cfg.envMemoryMB != nil { - envElems = append(envElems, bson.E{Key: "memory_mb", Value: *cfg.envMemoryMB}) - } - - if cfg.envRegion != "" { - envElems = append(envElems, bson.E{Key: "region", Value: cfg.envRegion}) - } - - if cfg.envTimeoutSec != nil { - envElems = append(envElems, bson.E{Key: "timeout_sec", Value: *cfg.envTimeoutSec}) - } - - if len(envElems) > 0 { - elems = append(elems, bson.E{Key: "env", Value: envElems}) - } - - bytes, err := bson.Marshal(elems) - require.NoError(t, err) - - return bytes -} From 8d6b75d8ba2a1b2ea640cf1ea1f5040e30e3d773 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:49:34 -0700 Subject: [PATCH 04/13] Use statically-defined expected metadata documents for all tests. --- internal/integration/handshake_test.go | 171 ++++++++++++++++++++----- 1 file changed, 141 insertions(+), 30 deletions(-) diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index c9e4c1f725..d2da0cdf07 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -732,7 +732,7 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes require.NoError(mt, err, "Ping error: %v", err) // 4. Wait 5ms for the connection to become idle. - time.Sleep(5 * time.Millisecond) + time.Sleep(20 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -749,13 +749,25 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes require.NoError(mt, err, "Ping error: %v", err) // 7. Save intercepted `client` document as `clientMetadata`. - clientMetadata := mt.GetProxyCapture().TryNext() - - require.NotNil(mt, clientMetadata, "expected to capture a proxied message") - assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + // + // 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) + time.Sleep(20 * time.Millisecond) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() @@ -773,7 +785,7 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) } // Test 5: Metadata is not appended if identical to initial metadata @@ -804,12 +816,24 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical(t *testing.T) { err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) - clientMetadata := mt.GetProxyCapture().TryNext() - require.NotNil(mt, clientMetadata, "expected to capture a proxied message") - assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + // 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) + time.Sleep(20 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -830,7 +854,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical(t *testing.T) { require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) } // Test 6: Metadata is not appended if identical to initial metadata (separated @@ -863,7 +887,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t require.NoError(mt, err, "Ping error: %v", err) // 3. Wait 5ms for the connection to become idle. - time.Sleep(5 * time.Millisecond) + time.Sleep(20 * time.Millisecond) // 4. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -880,12 +904,25 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t require.NoError(mt, err, "Ping error: %v", err) // 6. Save intercepted `client` document as `clientMetadata`. - clientMetadata := mt.GetProxyCapture().TryNext() - require.NotNil(mt, clientMetadata, "expected to capture a proxied message") - assert.True(mt, clientMetadata.IsHandshake(), "expected first message to be a handshake") + // + // 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) + time.Sleep(20 * time.Millisecond) // 8. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -907,7 +944,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, clientMetadata.Sent.Command, updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) } // Test 7: Empty strings are considered unset when appending duplicate metadata. @@ -918,6 +955,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { name string initialDriverInfo options.DriverInfo toAppendDriverInfo options.DriverInfo + want []byte }{ { name: "name empty", @@ -931,6 +969,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { 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", @@ -944,6 +993,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { 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", @@ -957,6 +1017,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { 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()}, + }), }, } @@ -985,10 +1056,14 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { require.NoError(mt, err, "Ping error: %v", err) // 4. 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") + // + // 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(20 * time.Millisecond) @@ -1010,8 +1085,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, initialClientMetadata.Sent.Command, - updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, tc.want, updatedClientMetadata.Sent.Command) }) } } @@ -1025,6 +1099,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing name string initialDriverInfo options.DriverInfo toAppendDriverInfo options.DriverInfo + want []byte }{ { name: "name empty", @@ -1038,6 +1113,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing 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", @@ -1051,6 +1137,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing 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", @@ -1064,6 +1161,17 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing 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()}, + }), }, } @@ -1089,10 +1197,14 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing require.NoError(mt, err, "Ping error: %v", err) // 3. 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") + // + // 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(20 * time.Millisecond) @@ -1115,8 +1227,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") // 8. Assert that `initialClientMetadata` is identical to `updatedClientMetadata`. - assertbsoncore.HandshakeClientMetadata(mt, initialClientMetadata.Sent.Command, - updatedClientMetadata.Sent.Command) + assertbsoncore.HandshakeClientMetadata(mt, tc.want, updatedClientMetadata.Sent.Command) }) } } From bc9a4131310f4875af83d16c0da4b861b23d490d Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:17:27 -0700 Subject: [PATCH 05/13] Fix TestAuthenticateToAnything and reduce calls to atomic.Pointer.Load --- x/mongo/driver/topology/topology_options.go | 34 ++++++++++--------- .../driver/topology/topology_options_test.go | 2 +- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 2e46d2c7ef..0cbe1c8fab 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -292,10 +292,12 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au ClusterClock: clock, } - if driverInfo := settings.driverInfo; driverInfo != nil && driverInfo.Load() != nil { - handshakeOpts.OuterLibraryName = driverInfo.Load().Name - handshakeOpts.OuterLibraryVersion = driverInfo.Load().Version - handshakeOpts.OuterLibraryPlatform = driverInfo.Load().Platform + if settings.driverInfo != nil { + if di := settings.driverInfo.Load(); di != nil { + handshakeOpts.OuterLibraryName = di.Name + handshakeOpts.OuterLibraryVersion = di.Version + handshakeOpts.OuterLibraryPlatform = di.Platform + } } if opts.Auth.AuthMechanism == "" { @@ -317,22 +319,22 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au } else { handshaker = func(driver.Handshaker) driver.Handshaker { - var outerLibraryName, outerLibraryVersion, outerLibraryPlatform string - if driverInfo := settings.driverInfo; driverInfo != nil && driverInfo.Load() != nil { - outerLibraryName = driverInfo.Load().Name - outerLibraryVersion = driverInfo.Load().Version - outerLibraryPlatform = driverInfo.Load().Platform - } - - return operation.NewHello(). + op := operation.NewHello(). AppName(appName). Compressors(comps). ClusterClock(clock). ServerAPI(serverAPI). - LoadBalanced(loadBalanced). - OuterLibraryName(outerLibraryName). - OuterLibraryVersion(outerLibraryVersion). - OuterLibraryPlatform(outerLibraryPlatform) + LoadBalanced(loadBalanced) + + if settings.driverInfo != nil { + if di := settings.driverInfo.Load(); di != nil { + op = op.OuterLibraryName(di.Name). + OuterLibraryVersion(di.Version). + OuterLibraryPlatform(di.Platform) + } + } + + return op } } diff --git a/x/mongo/driver/topology/topology_options_test.go b/x/mongo/driver/topology/topology_options_test.go index 402503b300..319dabf9c2 100644 --- a/x/mongo/driver/topology/topology_options_test.go +++ b/x/mongo/driver/topology/topology_options_test.go @@ -149,7 +149,7 @@ func TestAuthenticateToAnything(t *testing.T) { opt := options.Client().SetAuth(options.Credential{Username: "foo", Password: "bar"}) err := tc.set(opt) require.NoError(t, err, "error setting authenticateToAnything: %v", err) - cfg, err := NewAuthenticatorConfig(nil, WithAuthConfigClientOptions(opt)) + cfg, err := NewAuthenticatorConfig(&testAuthenticator{}, WithAuthConfigClientOptions(opt)) require.NoError(t, err, "error constructing topology config: %v", err) srvrCfg := newServerConfig(defaultConnectionTimeout, cfg.ServerOpts...) From 74be46f274a9a5ce1d5077cea7f688ac9da86812 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:52:03 -0700 Subject: [PATCH 06/13] Move assert.EqualBSON to the assertbson package and use it instead of assertbsoncore. --- internal/assert/assertbson/assertbson.go | 55 ++++++++++++++++ internal/assert/assertbson/assertbson_test.go | 62 +++++++++++++++++++ .../assertbsoncore/assertions_bsoncore.go | 47 -------------- internal/assert/assertion_mongo.go | 21 ------- internal/assert/assertion_mongo_test.go | 51 --------------- internal/handshake/handshake.go | 21 ------- internal/integration/client_test.go | 7 ++- internal/integration/handshake_test.go | 41 +++++++++--- mongo/mongo_test.go | 3 +- 9 files changed, 156 insertions(+), 152 deletions(-) create mode 100644 internal/assert/assertbson/assertbson.go create mode 100644 internal/assert/assertbson/assertbson_test.go delete mode 100644 internal/assert/assertbsoncore/assertions_bsoncore.go diff --git a/internal/assert/assertbson/assertbson.go b/internal/assert/assertbson/assertbson.go new file mode 100644 index 0000000000..247d33ac37 --- /dev/null +++ b/internal/assert/assertbson/assertbson.go @@ -0,0 +1,55 @@ +// Copyright (C) MongoDB, Inc. 2025-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 assertbson + +import ( + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/internal/assert" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" +) + +type tHelper interface { + Helper() +} + +// EqualDocument asserts that the expected and actual BSON documents are equal. +// If the documents are not equal, it prints both the binary diff and Extended +// JSON representation of the BSON documents. +func EqualDocument(t assert.TestingT, expected, actual []byte) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + return assert.Equal(t, + expected, + actual, + `expected and actual BSON documents do not match +As Extended JSON: +Expected: %s +Actual : %s`, + bson.Raw(expected), + bson.Raw(actual)) +} + +// EqualValue asserts that the expected and actual BSON values are equal. If the +// values are not equal, it prints both the binary diff and Extended JSON +// representation of the BSON values. +func EqualValue[T bson.RawValue | bsoncore.Value](t assert.TestingT, expected, actual T) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + return assert.Equal(t, + expected, + actual, + `expected and actual BSON values do not match +As Extended JSON: +Expected: %s +Actual : %s`, + expected, + actual) +} diff --git a/internal/assert/assertbson/assertbson_test.go b/internal/assert/assertbson/assertbson_test.go new file mode 100644 index 0000000000..0e2f6cfd65 --- /dev/null +++ b/internal/assert/assertbson/assertbson_test.go @@ -0,0 +1,62 @@ +// Copyright (C) MongoDB, Inc. 2025-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 assertbson + +import ( + "testing" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +func TestEqualDocument(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + expected []byte + actual []byte + want bool + }{ + { + name: "equal bson.Raw", + expected: bson.Raw{5, 0, 0, 0, 0}, + actual: bson.Raw{5, 0, 0, 0, 0}, + want: true, + }, + { + name: "different bson.Raw", + expected: bson.Raw{8, 0, 0, 0, 10, 120, 0, 0}, + actual: bson.Raw{5, 0, 0, 0, 0}, + want: false, + }, + { + name: "invalid bson.Raw", + expected: bson.Raw{99, 99, 99, 99}, + actual: bson.Raw{5, 0, 0, 0, 0}, + want: false, + }, + { + name: "nil bson.Raw", + expected: bson.Raw(nil), + actual: bson.Raw(nil), + want: true, + }, + } + + for _, tc := range testCases { + tc := tc // Capture range variable. + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := EqualDocument(new(testing.T), tc.expected, tc.actual) + if got != tc.want { + t.Errorf("EqualBSON(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) + } + }) + } +} diff --git a/internal/assert/assertbsoncore/assertions_bsoncore.go b/internal/assert/assertbsoncore/assertions_bsoncore.go deleted file mode 100644 index 872192d922..0000000000 --- a/internal/assert/assertbsoncore/assertions_bsoncore.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (C) MongoDB, Inc. 2025-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 assertbsoncore - -import ( - "errors" - "testing" - - "go.mongodb.org/mongo-driver/v2/internal/assert" - "go.mongodb.org/mongo-driver/v2/internal/handshake" - "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" -) - -// HandshakeClientMetadata compares the client metadata in two wire messages. It -// extracts the client metadata document from each wire message and compares -// them. If the document is not found, it assumes the wire message is just the -// value of the client metadata document itself. -func HandshakeClientMetadata(t testing.TB, expectedWM, actualWM []byte) bool { - gotCommand, err := handshake.ParseClientMetadata(actualWM) - if err != nil { - if errors.Is(err, bsoncore.ErrElementNotFound) { - // If the element is not found, the actual wire message may just be the - // client metadata document itself. - gotCommand = bsoncore.Document(actualWM) - } else { - return assert.Fail(t, "error parsing actual wire message: %v", err) - } - } - - wantCommand, err := handshake.ParseClientMetadata(expectedWM) - if err != nil { - // If the element is not found, the expected wire message may just be the - // client metadata document itself. - if errors.Is(err, bsoncore.ErrElementNotFound) { - wantCommand = bsoncore.Document(expectedWM) - } else { - return assert.Fail(t, "error parsing expected wire message: %v", err) - } - } - - return assert.Equal(t, wantCommand, gotCommand, - "expected: %v, got: %v", bsoncore.Document(wantCommand), bsoncore.Document(gotCommand)) -} diff --git a/internal/assert/assertion_mongo.go b/internal/assert/assertion_mongo.go index e47fdf93e1..76e5c37e5e 100644 --- a/internal/assert/assertion_mongo.go +++ b/internal/assert/assertion_mongo.go @@ -11,7 +11,6 @@ package assert import ( "context" - "fmt" "reflect" "time" "unsafe" @@ -71,26 +70,6 @@ func DifferentAddressRanges(t TestingT, a, b []byte) (ok bool) { return false } -// EqualBSON asserts that the expected and actual BSON binary values are equal. -// If the values are not equal, it prints both the binary and Extended JSON diff -// of the BSON values. The provided BSON value types must implement the -// fmt.Stringer interface. -func EqualBSON(t TestingT, expected, actual interface{}) bool { - if h, ok := t.(tHelper); ok { - h.Helper() - } - - return Equal(t, - expected, - actual, - `expected and actual BSON values do not match -As Extended JSON: -Expected: %s -Actual : %s`, - expected.(fmt.Stringer).String(), - actual.(fmt.Stringer).String()) -} - // Soon runs the provided callback and fails the passed-in test if the callback // does not complete within timeout. The provided callback should respect the // passed-in context and cease execution when it has expired. diff --git a/internal/assert/assertion_mongo_test.go b/internal/assert/assertion_mongo_test.go index 9fe6f485d5..490adcef3d 100644 --- a/internal/assert/assertion_mongo_test.go +++ b/internal/assert/assertion_mongo_test.go @@ -8,8 +8,6 @@ package assert import ( "testing" - - "go.mongodb.org/mongo-driver/v2/bson" ) func TestDifferentAddressRanges(t *testing.T) { @@ -74,52 +72,3 @@ func TestDifferentAddressRanges(t *testing.T) { }) } } - -func TestEqualBSON(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - expected interface{} - actual interface{} - want bool - }{ - { - name: "equal bson.Raw", - expected: bson.Raw{5, 0, 0, 0, 0}, - actual: bson.Raw{5, 0, 0, 0, 0}, - want: true, - }, - { - name: "different bson.Raw", - expected: bson.Raw{8, 0, 0, 0, 10, 120, 0, 0}, - actual: bson.Raw{5, 0, 0, 0, 0}, - want: false, - }, - { - name: "invalid bson.Raw", - expected: bson.Raw{99, 99, 99, 99}, - actual: bson.Raw{5, 0, 0, 0, 0}, - want: false, - }, - { - name: "nil bson.Raw", - expected: bson.Raw(nil), - actual: bson.Raw(nil), - want: true, - }, - } - - for _, tc := range testCases { - tc := tc // Capture range variable. - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - got := EqualBSON(new(testing.T), tc.expected, tc.actual) - if got != tc.want { - t.Errorf("EqualBSON(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) - } - }) - } -} diff --git a/internal/handshake/handshake.go b/internal/handshake/handshake.go index f66fd8d34f..c9537d3ef8 100644 --- a/internal/handshake/handshake.go +++ b/internal/handshake/handshake.go @@ -6,29 +6,8 @@ package handshake -import ( - "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" -) - // LegacyHello is the legacy version of the hello command. var LegacyHello = "isMaster" // LegacyHelloLowercase is the lowercase, legacy version of the hello command. var LegacyHelloLowercase = "ismaster" - -func ParseClientMetadata(msg []byte) ([]byte, error) { - command := bsoncore.Document(msg) - - // Lookup the "client" field in the command document. - clientMetadataRaw, err := command.LookupErr("client") - if err != nil { - return nil, err - } - - clientMetadata, ok := clientMetadataRaw.DocumentOK() - if !ok { - return nil, err - } - - return clientMetadata, nil -} diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 37337f6d86..72fcc3e9fe 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -20,7 +20,7 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/event" "go.mongodb.org/mongo-driver/v2/internal/assert" - "go.mongodb.org/mongo-driver/v2/internal/assert/assertbsoncore" + "go.mongodb.org/mongo-driver/v2/internal/assert/assertbson" "go.mongodb.org/mongo-driver/v2/internal/eventtest" "go.mongodb.org/mongo-driver/v2/internal/failpoint" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" @@ -477,7 +477,8 @@ func TestClient(t *testing.T) { message := mt.GetProxyCapture().TryNext() require.NotNil(mt, message, "expected handshake message, got nil") - assertbsoncore.HandshakeClientMetadata(mt, want, message.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, message.Sent.Command) + assertbson.EqualDocument(mt, want, clientMetadata) } }) @@ -1096,7 +1097,7 @@ func TestClient_BSONOptions(t *testing.T) { got, err := sr.Raw() require.NoError(mt, err, "Raw error") - assert.EqualBSON(mt, tc.wantRaw, got) + assertbson.EqualDocument(mt, tc.wantRaw, got) } }) } diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index d2da0cdf07..fe122ed520 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -15,11 +15,12 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" - "go.mongodb.org/mongo-driver/v2/internal/assert/assertbsoncore" + "go.mongodb.org/mongo-driver/v2/internal/assert/assertbson" "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" ) @@ -277,7 +278,9 @@ func TestHandshakeProse(t *testing.T) { require.NotNil(mt, firstMessage, "expected to capture a proxied message") assert.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, tc.want, firstMessage.Sent.Command) + + clientMetadata := clientMetadataFromHandshake(mt, firstMessage.Sent.Command) + assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } @@ -699,7 +702,8 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { require.NotNil(mt, gotMessage, "expected to capture a proxied message") assert.True(mt, gotMessage.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, tc.want, gotMessage.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, gotMessage.Sent.Command) + assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } @@ -785,7 +789,8 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) + assertbson.EqualDocument(mt, want, clientMetadata) } // Test 5: Metadata is not appended if identical to initial metadata @@ -854,7 +859,9 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical(t *testing.T) { require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) + assertbson.EqualDocument(mt, want, clientMetadata) + } // Test 6: Metadata is not appended if identical to initial metadata (separated @@ -944,7 +951,8 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, want, updatedClientMetadata.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) + assertbson.EqualDocument(mt, want, clientMetadata) } // Test 7: Empty strings are considered unset when appending duplicate metadata. @@ -1085,7 +1093,8 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") - assertbsoncore.HandshakeClientMetadata(mt, tc.want, updatedClientMetadata.Sent.Command) + clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) + assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } @@ -1227,11 +1236,13 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") // 8. Assert that `initialClientMetadata` is identical to `updatedClientMetadata`. - assertbsoncore.HandshakeClientMetadata(mt, tc.want, updatedClientMetadata.Sent.Command) + 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 { @@ -1240,3 +1251,17 @@ func mustMarshalBSON(val interface{}) []byte { 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 +} diff --git a/mongo/mongo_test.go b/mongo/mongo_test.go index f0cb6125dc..933743e1a3 100644 --- a/mongo/mongo_test.go +++ b/mongo/mongo_test.go @@ -14,6 +14,7 @@ import ( "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/codecutil" "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/mongo/options" @@ -603,7 +604,7 @@ func TestMarshalValue(t *testing.T) { t.Parallel() got, err := marshalValue(tc.value, tc.bsonOpts, tc.registry) - assert.EqualBSON(t, tc.want, got) + assertbson.EqualValue(t, tc.want, got) assert.Equal(t, tc.wantErr, err, "expected and actual error do not match") }) } From 8b483fdce294e6e4003f39df95671e47380c01ed Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:07:32 -0700 Subject: [PATCH 07/13] Fix appending client metadata for authenticated handshakes. --- x/mongo/driver/topology/topology_options.go | 60 +++++++++++---------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/x/mongo/driver/topology/topology_options.go b/x/mongo/driver/topology/topology_options.go index 0cbe1c8fab..4358d5049e 100644 --- a/x/mongo/driver/topology/topology_options.go +++ b/x/mongo/driver/topology/topology_options.go @@ -188,6 +188,7 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au opts := settings.opts clock := settings.clock + driverInfo := settings.driverInfo var serverAPI *driver.ServerAPIOptions @@ -242,8 +243,8 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au })) } - if settings.driverInfo != nil { - serverOpts = append(serverOpts, WithDriverInfo(settings.driverInfo)) + if driverInfo != nil { + serverOpts = append(serverOpts, WithDriverInfo(driverInfo)) } // Compressors & ZlibLevel @@ -283,37 +284,38 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au // Handshaker var handshaker func(driver.Handshaker) driver.Handshaker if authenticator != nil { - handshakeOpts := &auth.HandshakeOptions{ - AppName: appName, - Authenticator: authenticator, - Compressors: comps, - ServerAPI: serverAPI, - LoadBalanced: loadBalanced, - ClusterClock: clock, - } + handshaker = func(driver.Handshaker) driver.Handshaker { + handshakeOpts := &auth.HandshakeOptions{ + AppName: appName, + Authenticator: authenticator, + Compressors: comps, + ServerAPI: serverAPI, + LoadBalanced: loadBalanced, + ClusterClock: clock, + } - if settings.driverInfo != nil { - if di := settings.driverInfo.Load(); di != nil { - handshakeOpts.OuterLibraryName = di.Name - handshakeOpts.OuterLibraryVersion = di.Version - handshakeOpts.OuterLibraryPlatform = di.Platform + if opts.Auth.AuthMechanism == "" { + // Required for SASL mechanism negotiation during handshake + handshakeOpts.DBUser = opts.Auth.AuthSource + "." + opts.Auth.Username } - } - if opts.Auth.AuthMechanism == "" { - // Required for SASL mechanism negotiation during handshake - handshakeOpts.DBUser = opts.Auth.AuthSource + "." + opts.Auth.Username - } - if a := optionsutil.Value(opts.Custom, "authenticateToAnything"); a != nil { - if v, ok := a.(bool); ok && v { - // Authenticate arbiters - handshakeOpts.PerformAuthentication = func(_ description.Server) bool { - return true + if a := optionsutil.Value(opts.Custom, "authenticateToAnything"); a != nil { + if v, ok := a.(bool); ok && v { + // Authenticate arbiters + handshakeOpts.PerformAuthentication = func(_ description.Server) bool { + return true + } + } + } + + if driverInfo != nil { + if di := driverInfo.Load(); di != nil { + handshakeOpts.OuterLibraryName = di.Name + handshakeOpts.OuterLibraryVersion = di.Version + handshakeOpts.OuterLibraryPlatform = di.Platform } } - } - handshaker = func(driver.Handshaker) driver.Handshaker { return auth.Handshaker(nil, handshakeOpts) } @@ -326,8 +328,8 @@ func NewAuthenticatorConfig(authenticator driver.Authenticator, clientOpts ...Au ServerAPI(serverAPI). LoadBalanced(loadBalanced) - if settings.driverInfo != nil { - if di := settings.driverInfo.Load(); di != nil { + if driverInfo != nil { + if di := driverInfo.Load(); di != nil { op = op.OuterLibraryName(di.Name). OuterLibraryVersion(di.Version). OuterLibraryPlatform(di.Platform) From 2cce6d6ea4e8f49a28a5ead5cf4e5a1836f46772 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Wed, 15 Oct 2025 22:48:21 -0700 Subject: [PATCH 08/13] Fix app name test. --- internal/integration/client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/integration/client_test.go b/internal/integration/client_test.go index 5fcdd5f6c1..b51ea5bd10 100644 --- a/internal/integration/client_test.go +++ b/internal/integration/client_test.go @@ -460,6 +460,9 @@ func TestClient(t *testing.T) { assert.Nil(mt, err, "Ping error: %v", err) want := mustMarshalBSON(bson.D{ + {Key: "application", Value: bson.D{ + bson.E{Key: "name", Value: "foo"}, + }}, {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, @@ -469,9 +472,6 @@ func TestClient(t *testing.T) { {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, - {Key: "application", Value: bson.D{ - bson.E{Key: "name", Value: "foo"}, - }}, }) for i := 0; i < 2; i++ { From 551ff2e11e956665d559604be9cc23a28344e14e Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:11:35 -0700 Subject: [PATCH 09/13] Fix LB handshake test and PR feedback. --- internal/integration/handshake_test.go | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index fe122ed520..0a527fd21e 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -16,6 +16,7 @@ import ( "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" @@ -261,6 +262,8 @@ func TestHandshakeProse(t *testing.T) { } 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) @@ -301,7 +304,7 @@ func TestLoadBalancedConnectionHandshake(t *testing.T) { // Per the specifications, if loadBalanced=true, drivers MUST use the hello // command for the initial handshake and use the OP_MSG protocol. - assert.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") + assert.Equal(mt, "hello", firstMessage.CommandName) assert.Equal(mt, wiremessage.OpMsg, firstMessage.Sent.OpCode) }) @@ -320,13 +323,17 @@ func TestLoadBalancedConnectionHandshake(t *testing.T) { 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.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") + assert.Equal(mt, hello, firstMessage, "expected first message to be a handshake") assert.Equal(mt, want, firstMessage.Sent.OpCode) }) } @@ -655,6 +662,8 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { } 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) @@ -686,7 +695,7 @@ func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { assert.True(mt, initialClientMetadata.IsHandshake(), "expected first message to be a handshake") // Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) mt.Client.AppendDriverInfo(tc.driverInfo) @@ -736,7 +745,7 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes require.NoError(mt, err, "Ping error: %v", err) // 4. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -771,7 +780,7 @@ func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *tes }) // 8. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() @@ -838,7 +847,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical(t *testing.T) { }) // 3. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -894,7 +903,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t require.NoError(mt, err, "Ping error: %v", err) // 3. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 4. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -929,7 +938,7 @@ func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *t }) // 7. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 8. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ @@ -1040,6 +1049,8 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { } 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) @@ -1074,7 +1085,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { // metadata value. // 5. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 6. Append the `DriverInfoOptions` from the selected test case from // the appended metadata section. @@ -1216,7 +1227,7 @@ func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing // metadata value. // 4. Wait 5ms for the connection to become idle. - time.Sleep(20 * time.Millisecond) + time.Sleep(5 * time.Millisecond) // 5. Append the `DriverInfoOptions` from the selected test case from // the appended metadata section. From 9fa60964c252cdbfa0ac4142becb454099a3a2cd Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:24:55 -0700 Subject: [PATCH 10/13] Fix AppendDriverInfo comment. --- mongo/client.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mongo/client.go b/mongo/client.go index d7aabde380..10182276b2 100644 --- a/mongo/client.go +++ b/mongo/client.go @@ -311,16 +311,11 @@ func (c *Client) connect() error { return nil } -// AppendDriverInfo appends the provided DriverInfo to the driver information -// that will be sent to the server in handshake requests when establishing new -// connections. The provided info will overwrite any existing values. -// -// AppendsDriverInfo appends the provided [options.DriverInfo] to the metadata +// AppendDriverInfo appends the provided [options.DriverInfo] to the metadata // (e.g. name, version, platform) that will be sent to the server in handshake -// requests when establishing new connections. The provided info will overwrite -// any existing values. +// requests when establishing new connections. // -// Repeated calls to appendMetadata with equivalent DriverInfo is a no-op. +// Repeated calls to AppendDriverInfo with equivalent DriverInfo is a no-op. // // Metadata is limited to 512 bytes; any excess will be truncated. func (c *Client) AppendDriverInfo(info options.DriverInfo) { From 0785b0d1a128099b71d58ff2d97435dc9d0f15fe Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:30:01 -0700 Subject: [PATCH 11/13] Fix non-LB handshake test. --- internal/integration/handshake_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/integration/handshake_test.go b/internal/integration/handshake_test.go index 0a527fd21e..d2756faa43 100644 --- a/internal/integration/handshake_test.go +++ b/internal/integration/handshake_test.go @@ -333,7 +333,7 @@ func TestLoadBalancedConnectionHandshake(t *testing.T) { want = wiremessage.OpMsg } - assert.Equal(mt, hello, firstMessage, "expected first message to be a handshake") + assert.Equal(mt, hello, firstMessage.CommandName, "expected first message to be a handshake") assert.Equal(mt, want, firstMessage.Sent.OpCode) }) } From 11282cdc81427d6fa3601a1e7ae4750c31c491ac Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:41:40 -0700 Subject: [PATCH 12/13] Update internal/assert/assertbson/assertbson_test.go Co-authored-by: Qingyang Hu <103950869+qingyang-hu@users.noreply.github.com> --- internal/assert/assertbson/assertbson_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/assert/assertbson/assertbson_test.go b/internal/assert/assertbson/assertbson_test.go index 0e2f6cfd65..16fcca4dc7 100644 --- a/internal/assert/assertbson/assertbson_test.go +++ b/internal/assert/assertbson/assertbson_test.go @@ -55,7 +55,7 @@ func TestEqualDocument(t *testing.T) { got := EqualDocument(new(testing.T), tc.expected, tc.actual) if got != tc.want { - t.Errorf("EqualBSON(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) + t.Errorf("EqualDocument(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) } }) } From 11797dc5b1f0fcd6e8270794d6c98c8e34ce8143 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:41:23 -0700 Subject: [PATCH 13/13] Add test cases for assertbson.EqualValue --- internal/assert/assertbson/assertbson_test.go | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/internal/assert/assertbson/assertbson_test.go b/internal/assert/assertbson/assertbson_test.go index 16fcca4dc7..e553795750 100644 --- a/internal/assert/assertbson/assertbson_test.go +++ b/internal/assert/assertbson/assertbson_test.go @@ -10,6 +10,7 @@ import ( "testing" "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" ) func TestEqualDocument(t *testing.T) { @@ -60,3 +61,189 @@ func TestEqualDocument(t *testing.T) { }) } } + +func TestEqualValue(t *testing.T) { + t.Parallel() + + t.Run("bson.RawValue", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + expected bson.RawValue + actual bson.RawValue + want bool + }{ + { + name: "equal", + expected: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 0, 0, 0}, + }, + actual: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 0, 0, 0}, + }, + want: true, + }, + { + name: "same type, different value", + expected: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 0, 0, 0}, + }, + actual: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 1, 1, 1}, + }, + want: false, + }, + { + name: "same value, different type", + expected: bson.RawValue{ + Type: bson.TypeDouble, + Value: []byte{1, 0, 0, 0, 0, 0, 0, 0}, + }, + actual: bson.RawValue{ + Type: bson.TypeInt64, + Value: []byte{1, 0, 0, 0, 0, 0, 0, 0}, + }, + want: false, + }, + { + name: "different value, different type", + expected: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 0, 0, 0}, + }, + actual: bson.RawValue{ + Type: bson.TypeString, + Value: []byte{1, 1, 1, 1}, + }, + want: false, + }, + { + name: "invalid", + expected: bson.RawValue{ + Type: bson.TypeInt64, + Value: []byte{1, 0, 0, 0}, + }, + actual: bson.RawValue{ + Type: bson.TypeInt32, + Value: []byte{1, 0, 0, 0}, + }, + want: false, + }, + { + name: "empty", + expected: bson.RawValue{}, + actual: bson.RawValue{}, + want: true, + }, + } + + for _, tc := range testCases { + tc := tc // Capture range variable. + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := EqualValue(new(testing.T), tc.expected, tc.actual) + if got != tc.want { + t.Errorf("EqualValue(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) + } + }) + } + }) + + t.Run("bsoncore.Value", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + expected bsoncore.Value + actual bsoncore.Value + want bool + }{ + { + name: "equal", + expected: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 0, 0, 0}, + }, + actual: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 0, 0, 0}, + }, + want: true, + }, + { + name: "same type, different value", + expected: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 0, 0, 0}, + }, + actual: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 1, 1, 1}, + }, + want: false, + }, + { + name: "same value, different type", + expected: bsoncore.Value{ + Type: bsoncore.TypeDouble, + Data: []byte{1, 0, 0, 0, 0, 0, 0, 0}, + }, + actual: bsoncore.Value{ + Type: bsoncore.TypeInt64, + Data: []byte{1, 0, 0, 0, 0, 0, 0, 0}, + }, + want: false, + }, + { + name: "different value, different type", + expected: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 0, 0, 0}, + }, + actual: bsoncore.Value{ + Type: bsoncore.TypeString, + Data: []byte{1, 1, 1, 1}, + }, + want: false, + }, + { + name: "invalid", + expected: bsoncore.Value{ + Type: bsoncore.TypeInt64, + Data: []byte{1, 0, 0, 0}, + }, + actual: bsoncore.Value{ + Type: bsoncore.TypeInt32, + Data: []byte{1, 0, 0, 0}, + }, + want: false, + }, + { + name: "empty", + expected: bsoncore.Value{}, + actual: bsoncore.Value{}, + want: true, + }, + } + + for _, tc := range testCases { + tc := tc // Capture range variable. + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := EqualValue(new(testing.T), tc.expected, tc.actual) + if got != tc.want { + t.Errorf("EqualValue(%#v, %#v) = %v, want %v", tc.expected, tc.actual, got, tc.want) + } + }) + } + }) +}