...

Source file src/go.mongodb.org/mongo-driver/mongo/integration/errors_test.go

Documentation: go.mongodb.org/mongo-driver/mongo/integration

     1  // Copyright (C) MongoDB, Inc. 2017-present.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License"); you may
     4  // not use this file except in compliance with the License. You may obtain
     5  // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
     6  
     7  //go:build go1.13
     8  // +build go1.13
     9  
    10  package integration
    11  
    12  import (
    13  	"context"
    14  	"errors"
    15  	"fmt"
    16  	"io"
    17  	"net"
    18  	"testing"
    19  	"time"
    20  
    21  	"go.mongodb.org/mongo-driver/bson"
    22  	"go.mongodb.org/mongo-driver/internal/assert"
    23  	"go.mongodb.org/mongo-driver/internal/integtest"
    24  	"go.mongodb.org/mongo-driver/mongo"
    25  	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
    26  	"go.mongodb.org/mongo-driver/mongo/options"
    27  	"go.mongodb.org/mongo-driver/x/mongo/driver"
    28  	"go.mongodb.org/mongo-driver/x/mongo/driver/topology"
    29  )
    30  
    31  type netErr struct {
    32  	timeout bool
    33  }
    34  
    35  func (n netErr) Error() string {
    36  	return "error"
    37  }
    38  
    39  func (n netErr) Timeout() bool {
    40  	return n.timeout
    41  }
    42  
    43  func (n netErr) Temporary() bool {
    44  	return false
    45  }
    46  
    47  var _ net.Error = (*netErr)(nil)
    48  
    49  func TestErrors(t *testing.T) {
    50  	mt := mtest.New(t, noClientOpts)
    51  
    52  	mt.RunOpts("errors are wrapped", noClientOpts, func(mt *mtest.T) {
    53  		mt.Run("network error during application operation", func(mt *mtest.T) {
    54  			ctx, cancel := context.WithCancel(context.Background())
    55  			cancel()
    56  
    57  			err := mt.Client.Ping(ctx, mtest.PrimaryRp)
    58  			assert.True(mt, errors.Is(err, context.Canceled), "expected error %v, got %v", context.Canceled, err)
    59  		})
    60  
    61  		authOpts := mtest.NewOptions().Auth(true).Topologies(mtest.ReplicaSet, mtest.Single).MinServerVersion("4.0")
    62  		mt.RunOpts("network error during auth", authOpts, func(mt *mtest.T) {
    63  			mt.SetFailPoint(mtest.FailPoint{
    64  				ConfigureFailPoint: "failCommand",
    65  				Mode: mtest.FailPointMode{
    66  					Times: 1,
    67  				},
    68  				Data: mtest.FailPointData{
    69  					// Set the fail point for saslContinue rather than saslStart because the driver will use speculative
    70  					// auth on 4.4+ so there won't be an explicit saslStart command.
    71  					FailCommands:    []string{"saslContinue"},
    72  					CloseConnection: true,
    73  				},
    74  			})
    75  
    76  			clientOpts := options.Client().ApplyURI(mtest.ClusterURI())
    77  			integtest.AddTestServerAPIVersion(clientOpts)
    78  			client, err := mongo.Connect(context.Background(), clientOpts)
    79  			assert.Nil(mt, err, "Connect error: %v", err)
    80  			defer func() { _ = client.Disconnect(context.Background()) }()
    81  
    82  			// A connection getting closed should manifest as an io.EOF error.
    83  			err = client.Ping(context.Background(), mtest.PrimaryRp)
    84  			assert.True(mt, errors.Is(err, io.EOF), "expected error %v, got %v", io.EOF, err)
    85  		})
    86  	})
    87  
    88  	mt.RunOpts("network timeouts", noClientOpts, func(mt *mtest.T) {
    89  		mt.Run("context timeouts return DeadlineExceeded", func(mt *mtest.T) {
    90  			_, err := mt.Coll.InsertOne(context.Background(), bson.D{{"x", 1}})
    91  			assert.Nil(mt, err, "InsertOne error: %v", err)
    92  
    93  			mt.ClearEvents()
    94  			filter := bson.M{
    95  				"$where": "function() { sleep(1000); return false; }",
    96  			}
    97  			timeoutCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    98  			defer cancel()
    99  			_, err = mt.Coll.Find(timeoutCtx, filter)
   100  
   101  			evt := mt.GetStartedEvent()
   102  			assert.Equal(mt, "find", evt.CommandName, "expected command 'find', got %q", evt.CommandName)
   103  			assert.True(mt, errors.Is(err, context.DeadlineExceeded),
   104  				"errors.Is failure: expected error %v to be %v", err, context.DeadlineExceeded)
   105  		})
   106  
   107  		mt.Run("socketTimeoutMS timeouts return network errors", func(mt *mtest.T) {
   108  			_, err := mt.Coll.InsertOne(context.Background(), bson.D{{"x", 1}})
   109  			assert.Nil(mt, err, "InsertOne error: %v", err)
   110  
   111  			// Reset the test client to have a 100ms socket timeout. We do this here rather than passing it in as a
   112  			// test option using mt.RunOpts because that could cause the collection creation or InsertOne to fail.
   113  			resetClientOpts := options.Client().
   114  				SetSocketTimeout(100 * time.Millisecond)
   115  			mt.ResetClient(resetClientOpts)
   116  
   117  			mt.ClearEvents()
   118  			filter := bson.M{
   119  				"$where": "function() { sleep(1000); return false; }",
   120  			}
   121  			_, err = mt.Coll.Find(context.Background(), filter)
   122  
   123  			evt := mt.GetStartedEvent()
   124  			assert.Equal(mt, "find", evt.CommandName, "expected command 'find', got %q", evt.CommandName)
   125  
   126  			assert.False(mt, errors.Is(err, context.DeadlineExceeded),
   127  				"errors.Is failure: expected error %v to not be %v", err, context.DeadlineExceeded)
   128  			var netErr net.Error
   129  			ok := errors.As(err, &netErr)
   130  			assert.True(mt, ok, "errors.As failure: expected error %v to be a net.Error", err)
   131  			assert.True(mt, netErr.Timeout(), "expected error %v to be a network timeout", err)
   132  		})
   133  	})
   134  	mt.Run("ServerError", func(mt *mtest.T) {
   135  		matchWrapped := errors.New("wrapped err")
   136  		otherWrapped := errors.New("other err")
   137  		const matchCode = 100
   138  		const otherCode = 120
   139  		const label = "testError"
   140  		testCases := []struct {
   141  			name               string
   142  			err                mongo.ServerError
   143  			hasCode            bool
   144  			hasLabel           bool
   145  			hasMessage         bool
   146  			hasCodeWithMessage bool
   147  			isResult           bool
   148  		}{
   149  			{
   150  				"CommandError all true",
   151  				mongo.CommandError{matchCode, "foo", []string{label}, "name", matchWrapped, nil},
   152  				true,
   153  				true,
   154  				true,
   155  				true,
   156  				true,
   157  			},
   158  			{
   159  				"CommandError all false",
   160  				mongo.CommandError{otherCode, "bar", []string{"otherError"}, "name", otherWrapped, nil},
   161  				false,
   162  				false,
   163  				false,
   164  				false,
   165  				false,
   166  			},
   167  			{
   168  				"CommandError has code not message",
   169  				mongo.CommandError{matchCode, "bar", []string{}, "name", nil, nil},
   170  				true,
   171  				false,
   172  				false,
   173  				false,
   174  				false,
   175  			},
   176  			{
   177  				"WriteException all in writeConcernError",
   178  				mongo.WriteException{
   179  					&mongo.WriteConcernError{"name", matchCode, "foo", nil, nil},
   180  					nil,
   181  					[]string{label},
   182  					nil,
   183  				},
   184  				true,
   185  				true,
   186  				true,
   187  				true,
   188  				false,
   189  			},
   190  			{
   191  				"WriteException all in writeError",
   192  				mongo.WriteException{
   193  					nil,
   194  					mongo.WriteErrors{
   195  						mongo.WriteError{0, otherCode, "bar", nil, nil},
   196  						mongo.WriteError{0, matchCode, "foo", nil, nil},
   197  					},
   198  					[]string{"otherError"},
   199  					nil,
   200  				},
   201  				true,
   202  				false,
   203  				true,
   204  				true,
   205  				false,
   206  			},
   207  			{
   208  				"WriteException all false",
   209  				mongo.WriteException{
   210  					&mongo.WriteConcernError{"name", otherCode, "bar", nil, nil},
   211  					mongo.WriteErrors{
   212  						mongo.WriteError{0, otherCode, "baz", nil, nil},
   213  					},
   214  					[]string{"otherError"},
   215  					nil,
   216  				},
   217  				false,
   218  				false,
   219  				false,
   220  				false,
   221  				false,
   222  			},
   223  			{
   224  				"WriteException HasErrorCodeAndMessage false",
   225  				mongo.WriteException{
   226  					&mongo.WriteConcernError{"name", matchCode, "bar", nil, nil},
   227  					mongo.WriteErrors{
   228  						mongo.WriteError{0, otherCode, "foo", nil, nil},
   229  					},
   230  					[]string{"otherError"},
   231  					nil,
   232  				},
   233  				true,
   234  				false,
   235  				true,
   236  				false,
   237  				false,
   238  			},
   239  			{
   240  				"BulkWriteException all in writeConcernError",
   241  				mongo.BulkWriteException{
   242  					&mongo.WriteConcernError{"name", matchCode, "foo", nil, nil},
   243  					nil,
   244  					[]string{label},
   245  				},
   246  				true,
   247  				true,
   248  				true,
   249  				true,
   250  				false,
   251  			},
   252  			{
   253  				"BulkWriteException all in writeError",
   254  				mongo.BulkWriteException{
   255  					nil,
   256  					[]mongo.BulkWriteError{
   257  						{mongo.WriteError{0, matchCode, "foo", nil, nil}, &mongo.InsertOneModel{}},
   258  						{mongo.WriteError{0, otherCode, "bar", nil, nil}, &mongo.InsertOneModel{}},
   259  					},
   260  					[]string{"otherError"},
   261  				},
   262  				true,
   263  				false,
   264  				true,
   265  				true,
   266  				false,
   267  			},
   268  			{
   269  				"BulkWriteException all false",
   270  				mongo.BulkWriteException{
   271  					&mongo.WriteConcernError{"name", otherCode, "bar", nil, nil},
   272  					[]mongo.BulkWriteError{
   273  						{mongo.WriteError{0, otherCode, "baz", nil, nil}, &mongo.InsertOneModel{}},
   274  					},
   275  					[]string{"otherError"},
   276  				},
   277  				false,
   278  				false,
   279  				false,
   280  				false,
   281  				false,
   282  			},
   283  			{
   284  				"BulkWriteException HasErrorCodeAndMessage false",
   285  				mongo.BulkWriteException{
   286  					&mongo.WriteConcernError{"name", matchCode, "bar", nil, nil},
   287  					[]mongo.BulkWriteError{
   288  						{mongo.WriteError{0, otherCode, "foo", nil, nil}, &mongo.InsertOneModel{}},
   289  					},
   290  					[]string{"otherError"},
   291  				},
   292  				true,
   293  				false,
   294  				true,
   295  				false,
   296  				false,
   297  			},
   298  		}
   299  		for _, tc := range testCases {
   300  			mt.Run(tc.name, func(mt *mtest.T) {
   301  				has := tc.err.HasErrorCode(matchCode)
   302  				assert.Equal(mt, has, tc.hasCode, "expected HasErrorCode to return %v, got %v", tc.hasCode, has)
   303  				has = tc.err.HasErrorLabel(label)
   304  				assert.Equal(mt, has, tc.hasLabel, "expected HasErrorLabel to return %v, got %v", tc.hasLabel, has)
   305  
   306  				// Check for full message and substring
   307  				has = tc.err.HasErrorMessage("foo")
   308  				assert.Equal(mt, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has)
   309  				has = tc.err.HasErrorMessage("fo")
   310  				assert.Equal(mt, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has)
   311  				has = tc.err.HasErrorCodeWithMessage(matchCode, "foo")
   312  				assert.Equal(mt, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has)
   313  				has = tc.err.HasErrorCodeWithMessage(matchCode, "fo")
   314  				assert.Equal(mt, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has)
   315  
   316  				assert.Equal(mt, errors.Is(tc.err, matchWrapped), tc.isResult, "expected errors.Is result to be %v", tc.isResult)
   317  			})
   318  		}
   319  
   320  		mtOpts := mtest.NewOptions().MinServerVersion("4.0").Topologies(mtest.ReplicaSet)
   321  		mt.RunOpts("Raw response", mtOpts, func(mt *mtest.T) {
   322  			mt.Run("CommandError", func(mt *mtest.T) {
   323  				// Mock a CommandError via failpoint with an arbitrary code.
   324  				mt.SetFailPoint(mtest.FailPoint{
   325  					ConfigureFailPoint: "failCommand",
   326  					Mode: mtest.FailPointMode{
   327  						Times: 1,
   328  					},
   329  					Data: mtest.FailPointData{
   330  						FailCommands: []string{"find"},
   331  						ErrorCode:    123,
   332  					},
   333  				})
   334  
   335  				res := mt.Coll.FindOne(context.Background(), bson.D{})
   336  				assert.NotNil(mt, res.Err(), "expected FindOne error, got nil")
   337  				ce, ok := res.Err().(mongo.CommandError)
   338  				assert.True(mt, ok, "expected FindOne error to be CommandError, got %T", res.Err())
   339  
   340  				// Assert that raw response exists and contains error code 123.
   341  				assert.NotNil(mt, ce.Raw, "ce.Raw is nil")
   342  				val, err := ce.Raw.LookupErr("code")
   343  				assert.Nil(mt, err, "expected 'code' field in ce.Raw, got %v", ce.Raw)
   344  				code, ok := val.AsInt64OK()
   345  				assert.True(mt, ok, "expected 'code' to be int64, got %v", val)
   346  				assert.Equal(mt, code, int64(123), "expected 'code' 123, got %d", code)
   347  			})
   348  			mt.Run("WriteError", func(mt *mtest.T) {
   349  				// Mock a WriteError by inserting documents with duplicate _id fields.
   350  				_, err := mt.Coll.InsertOne(context.Background(), bson.D{{"_id", 1}})
   351  				assert.Nil(mt, err, "InsertOne error: %v", err)
   352  
   353  				_, err = mt.Coll.InsertOne(context.Background(), bson.D{{"_id", 1}})
   354  				assert.NotNil(mt, err, "expected InsertOne error, got nil")
   355  				we, ok := err.(mongo.WriteException)
   356  				assert.True(mt, ok, "expected InsertOne error to be WriteException, got %T", err)
   357  
   358  				assert.NotNil(mt, we.WriteErrors, "expected we.WriteErrors, got nil")
   359  				assert.NotNil(mt, we.WriteErrors[0], "expected at least one WriteError")
   360  
   361  				// Assert that raw response exists for the WriteError and contains error code 11000.
   362  				raw := we.WriteErrors[0].Raw
   363  				assert.NotNil(mt, raw, "Raw of WriteError is nil")
   364  				val, err := raw.LookupErr("code")
   365  				assert.Nil(mt, err, "expected 'code' field in Raw field, got %v", raw)
   366  				code, ok := val.AsInt64OK()
   367  				assert.True(mt, ok, "expected 'code' to be int64, got %v", val)
   368  				assert.Equal(mt, code, int64(11000), "expected 'code' 11000, got %d", code)
   369  			})
   370  			mt.Run("WriteException", func(mt *mtest.T) {
   371  				// Mock a WriteException via failpoint with an arbitrary WriteConcernError.
   372  				mt.SetFailPoint(mtest.FailPoint{
   373  					ConfigureFailPoint: "failCommand",
   374  					Mode: mtest.FailPointMode{
   375  						Times: 1,
   376  					},
   377  					Data: mtest.FailPointData{
   378  						FailCommands: []string{"delete"},
   379  						WriteConcernError: &mtest.WriteConcernErrorData{
   380  							Code: 123,
   381  						},
   382  					},
   383  				})
   384  
   385  				_, err := mt.Coll.DeleteMany(context.Background(), bson.D{})
   386  				assert.NotNil(mt, err, "expected DeleteMany error, got nil")
   387  				we, ok := err.(mongo.WriteException)
   388  				assert.True(mt, ok, "expected DeleteMany error to be WriteException, got %T", err)
   389  
   390  				// Assert that raw response exists and contains error code 123.
   391  				assert.NotNil(mt, we.Raw, "expected RawResponse, got nil")
   392  				val, err := we.Raw.LookupErr("writeConcernError", "code")
   393  				assert.Nil(mt, err, "expected 'code' field in RawResponse, got %v", we.Raw)
   394  				code, ok := val.AsInt64OK()
   395  				assert.True(mt, ok, "expected 'code' to be int64, got %v", val)
   396  				assert.Equal(mt, code, int64(123), "expected 'code' 123, got %d", code)
   397  			})
   398  		})
   399  	})
   400  	mt.Run("error helpers", func(mt *mtest.T) {
   401  		//IsDuplicateKeyError
   402  		mt.Run("IsDuplicateKeyError", func(mt *mtest.T) {
   403  			testCases := []struct {
   404  				name   string
   405  				err    error
   406  				result bool
   407  			}{
   408  				{"CommandError true", mongo.CommandError{11000, "", nil, "blah", nil, nil}, true},
   409  				{"CommandError false", mongo.CommandError{100, "", nil, "blah", nil, nil}, false},
   410  				{"WriteError true", mongo.WriteError{0, 11000, "", nil, nil}, true},
   411  				{"WriteError false", mongo.WriteError{0, 100, "", nil, nil}, false},
   412  				{
   413  					"WriteException true in writeConcernError",
   414  					mongo.WriteException{
   415  						&mongo.WriteConcernError{"name", 11001, "bar", nil, nil},
   416  						mongo.WriteErrors{
   417  							mongo.WriteError{0, 100, "baz", nil, nil},
   418  						},
   419  						nil,
   420  						nil,
   421  					},
   422  					true,
   423  				},
   424  				{
   425  					"WriteException true in writeErrors",
   426  					mongo.WriteException{
   427  						&mongo.WriteConcernError{"name", 100, "bar", nil, nil},
   428  						mongo.WriteErrors{
   429  							mongo.WriteError{0, 12582, "baz", nil, nil},
   430  						},
   431  						nil,
   432  						nil,
   433  					},
   434  					true,
   435  				},
   436  				{
   437  					"WriteException false",
   438  					mongo.WriteException{
   439  						&mongo.WriteConcernError{"name", 16460, "bar", nil, nil},
   440  						mongo.WriteErrors{
   441  							mongo.WriteError{0, 100, "blah E11000 blah", nil, nil},
   442  						},
   443  						nil,
   444  						nil,
   445  					},
   446  					false,
   447  				},
   448  				{
   449  					"BulkWriteException true",
   450  					mongo.BulkWriteException{
   451  						&mongo.WriteConcernError{"name", 100, "bar", nil, nil},
   452  						[]mongo.BulkWriteError{
   453  							{mongo.WriteError{0, 16460, "blah E11000 blah", nil, nil}, &mongo.InsertOneModel{}},
   454  						},
   455  						[]string{"otherError"},
   456  					},
   457  					true,
   458  				},
   459  				{
   460  					"BulkWriteException false",
   461  					mongo.BulkWriteException{
   462  						&mongo.WriteConcernError{"name", 100, "bar", nil, nil},
   463  						[]mongo.BulkWriteError{
   464  							{mongo.WriteError{0, 110, "blah", nil, nil}, &mongo.InsertOneModel{}},
   465  						},
   466  						[]string{"otherError"},
   467  					},
   468  					false,
   469  				},
   470  				{"wrapped error", fmt.Errorf("%w", mongo.CommandError{11000, "", nil, "blah", nil, nil}), true},
   471  				{"other error type", errors.New("foo"), false},
   472  			}
   473  			for _, tc := range testCases {
   474  				mt.Run(tc.name, func(mt *mtest.T) {
   475  					res := mongo.IsDuplicateKeyError(tc.err)
   476  					assert.Equal(mt, res, tc.result, "expected IsDuplicateKeyError %v, got %v", tc.result, res)
   477  				})
   478  			}
   479  		})
   480  		//IsNetworkError
   481  		mt.Run("IsNetworkError", func(mt *mtest.T) {
   482  			const networkLabel = "NetworkError"
   483  			const otherLabel = "other"
   484  			testCases := []struct {
   485  				name   string
   486  				err    error
   487  				result bool
   488  			}{
   489  				{"ServerError true", mongo.CommandError{100, "", []string{networkLabel}, "blah", nil, nil}, true},
   490  				{"ServerError false", mongo.CommandError{100, "", []string{otherLabel}, "blah", nil, nil}, false},
   491  				{"wrapped error", fmt.Errorf("%w", mongo.CommandError{100, "", []string{networkLabel}, "blah", nil, nil}), true},
   492  				{"other error type", errors.New("foo"), false},
   493  			}
   494  			for _, tc := range testCases {
   495  				mt.Run(tc.name, func(mt *mtest.T) {
   496  					res := mongo.IsNetworkError(tc.err)
   497  					assert.Equal(mt, res, tc.result, "expected IsNetworkError %v, got %v", tc.result, res)
   498  				})
   499  			}
   500  		})
   501  		//IsTimeout
   502  		mt.Run("IsTimeout", func(mt *mtest.T) {
   503  			testCases := []struct {
   504  				name   string
   505  				err    error
   506  				result bool
   507  			}{
   508  				{"context timeout", mongo.CommandError{
   509  					100, "", []string{"other"}, "blah", context.DeadlineExceeded, nil}, true},
   510  				{"deadline would be exceeded", mongo.CommandError{
   511  					100, "", []string{"other"}, "blah", driver.ErrDeadlineWouldBeExceeded, nil}, true},
   512  				{"server selection timeout", mongo.CommandError{
   513  					100, "", []string{"other"}, "blah", topology.ErrServerSelectionTimeout, nil}, true},
   514  				{"wait queue timeout", mongo.CommandError{
   515  					100, "", []string{"other"}, "blah", topology.WaitQueueTimeoutError{}, nil}, true},
   516  				{"ServerError NetworkTimeoutError", mongo.CommandError{
   517  					100, "", []string{"NetworkTimeoutError"}, "blah", nil, nil}, true},
   518  				{"ServerError ExceededTimeLimitError", mongo.CommandError{
   519  					100, "", []string{"ExceededTimeLimitError"}, "blah", nil, nil}, true},
   520  				{"ServerError false", mongo.CommandError{
   521  					100, "", []string{"other"}, "blah", nil, nil}, false},
   522  				{"net error true", mongo.CommandError{
   523  					100, "", []string{"other"}, "blah", netErr{true}, nil}, true},
   524  				{"net error false", netErr{false}, false},
   525  				{"wrapped error", fmt.Errorf("%w", mongo.CommandError{
   526  					100, "", []string{"other"}, "blah", context.DeadlineExceeded, nil}), true},
   527  				{"other error", errors.New("foo"), false},
   528  			}
   529  			for _, tc := range testCases {
   530  				mt.Run(tc.name, func(mt *mtest.T) {
   531  					res := mongo.IsTimeout(tc.err)
   532  					assert.Equal(mt, res, tc.result, "expected IsTimeout %v, got %v", tc.result, res)
   533  				})
   534  			}
   535  		})
   536  	})
   537  }
   538  

View as plain text