// Copyright (C) MongoDB, Inc. 2022-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 mongo import ( "context" "errors" "fmt" "net" "testing" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/x/mongo/driver" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/topology" ) func TestErrorMessages(t *testing.T) { details, err := bson.Marshal(bson.D{{"details", bson.D{{"operatorName", "$jsonSchema"}}}}) require.Nil(t, err, "unexpected error marshaling BSON") cases := []struct { desc string err error expected string }{ { desc: "WriteException error message should contain the WriteError Message and Details", err: WriteException{ WriteErrors: WriteErrors{ { Message: "test message 1", Details: details, }, { Message: "test message 2", Details: details, }, }, }, expected: `write exception: write errors: [test message 1: {"details": {"operatorName": "$jsonSchema"}}, test message 2: {"details": {"operatorName": "$jsonSchema"}}]`, }, { desc: "BulkWriteException error message should contain the WriteError Message and Details", err: BulkWriteException{ WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Message: "test message 1", Details: details, }, }, { WriteError: WriteError{ Message: "test message 2", Details: details, }, }, }, }, expected: `bulk write exception: write errors: [test message 1: {"details": {"operatorName": "$jsonSchema"}}, test message 2: {"details": {"operatorName": "$jsonSchema"}}]`, }, } for _, tc := range cases { tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() assert.Equal(t, tc.expected, tc.err.Error()) }) } } func TestServerError(t *testing.T) { matchWrapped := errors.New("wrapped err") otherWrapped := errors.New("other err") const matchCode = 100 const otherCode = 120 const label = "testError" testCases := []struct { name string err ServerError hasCode bool hasLabel bool hasMessage bool hasCodeWithMessage bool isResult bool }{ { name: "CommandError all true", err: CommandError{ Code: matchCode, Message: "foo", Labels: []string{label}, Name: "name", Wrapped: matchWrapped, }, hasCode: true, hasLabel: true, hasMessage: true, hasCodeWithMessage: true, isResult: true, }, { name: "CommandError all false", err: CommandError{ Code: otherCode, Message: "bar", Labels: []string{"otherError"}, Name: "name", Wrapped: otherWrapped, }, hasCode: false, hasLabel: false, hasMessage: false, hasCodeWithMessage: false, isResult: false, }, { name: "CommandError has code not message", err: CommandError{ Code: matchCode, Message: "bar", Labels: []string{}, Name: "name", }, hasCode: true, hasLabel: false, hasMessage: false, hasCodeWithMessage: false, isResult: false, }, { name: "WriteException all in writeConcernError", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: matchCode, Message: "foo", }, Labels: []string{label}, }, hasCode: true, hasLabel: true, hasMessage: true, hasCodeWithMessage: true, isResult: false, }, { name: "WriteException all in writeError", err: WriteException{ WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: otherCode, Message: "bar", }, WriteError{ Index: 0, Code: matchCode, Message: "foo", }, }, Labels: []string{"otherError"}, }, hasCode: true, hasLabel: false, hasMessage: true, hasCodeWithMessage: true, isResult: false, }, { name: "WriteException all false", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: otherCode, Message: "bar", }, WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: otherCode, Message: "baz", }, }, Labels: []string{"otherError"}, }, hasCode: false, hasLabel: false, hasMessage: false, hasCodeWithMessage: false, isResult: false, }, { name: "WriteException HasErrorCodeAndMessage false", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: matchCode, Message: "bar", }, WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: otherCode, Message: "foo", }, }, Labels: []string{"otherError"}, }, hasCode: true, hasLabel: false, hasMessage: true, hasCodeWithMessage: false, isResult: false, }, { name: "BulkWriteException all in writeConcernError", err: BulkWriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: matchCode, Message: "foo", }, Labels: []string{label}, }, hasCode: true, hasLabel: true, hasMessage: true, hasCodeWithMessage: true, isResult: false, }, { name: "BulkWriteException all in writeError", err: BulkWriteException{ WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Index: 0, Code: matchCode, Message: "foo", }, Request: &InsertOneModel{}, }, { WriteError: WriteError{ Index: 0, Code: otherCode, Message: "bar", }, Request: &InsertOneModel{}, }, }, Labels: []string{"otherError"}, }, hasCode: true, hasLabel: false, hasMessage: true, hasCodeWithMessage: true, isResult: false, }, { name: "BulkWriteException all false", err: BulkWriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: otherCode, Message: "bar", }, WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Index: 0, Code: otherCode, Message: "baz", }, Request: &InsertOneModel{}, }, }, Labels: []string{"otherError"}, }, hasCode: false, hasLabel: false, hasMessage: false, hasCodeWithMessage: false, isResult: false, }, { name: "BulkWriteException HasErrorCodeAndMessage false", err: BulkWriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: matchCode, Message: "bar", }, WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Index: 0, Code: otherCode, Message: "foo", }, Request: &InsertOneModel{}, }, }, Labels: []string{"otherError"}, }, hasCode: true, hasLabel: false, hasMessage: true, hasCodeWithMessage: false, isResult: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { has := tc.err.HasErrorCode(matchCode) assert.Equal(t, has, tc.hasCode, "expected HasErrorCode to return %v, got %v", tc.hasCode, has) has = tc.err.HasErrorLabel(label) assert.Equal(t, has, tc.hasLabel, "expected HasErrorLabel to return %v, got %v", tc.hasLabel, has) // Check for full message and substring has = tc.err.HasErrorMessage("foo") assert.Equal(t, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has) has = tc.err.HasErrorMessage("fo") assert.Equal(t, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has) has = tc.err.HasErrorCodeWithMessage(matchCode, "foo") assert.Equal(t, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has) has = tc.err.HasErrorCodeWithMessage(matchCode, "fo") assert.Equal(t, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has) assert.Equal(t, errors.Is(tc.err, matchWrapped), tc.isResult, "expected errors.Is result to be %v", tc.isResult) }) } } func TestIsDuplicateKeyError(t *testing.T) { testCases := []struct { name string err error result bool }{ { name: "CommandError true", err: CommandError{ Code: 11000, Name: "blah", }, result: true, }, { name: "CommandError false", err: CommandError{ Code: 100, Name: "blah", }, result: false, }, { name: "WriteError true", err: WriteError{ Index: 0, Code: 11000, }, result: true, }, { name: "WriteError false", err: WriteError{ Index: 0, Code: 100, }, result: false, }, { name: "WriteException true in writeConcernError", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: 11001, Message: "bar", }, WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: 100, Message: "baz", }, }, }, result: true, }, { name: "WriteException true in writeErrors", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: 100, Message: "bar", }, WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: 12582, Message: "baz", }, }, }, result: true, }, { name: "WriteException false", err: WriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: 16460, Message: "bar", }, WriteErrors: WriteErrors{ WriteError{ Index: 0, Code: 100, Message: "blah E11000 blah", }, }, }, result: false, }, { name: "BulkWriteException true", err: BulkWriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: 100, Message: "bar", }, WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Index: 0, Code: 16460, Message: "blah E11000 blah", }, Request: &InsertOneModel{}, }, }, Labels: []string{"otherError"}, }, result: true, }, { name: "BulkWriteException false", err: BulkWriteException{ WriteConcernError: &WriteConcernError{ Name: "name", Code: 100, Message: "bar", }, WriteErrors: []BulkWriteError{ { WriteError: WriteError{ Index: 0, Code: 110, Message: "blah", }, Request: &InsertOneModel{}, }, }, Labels: []string{"otherError"}, }, result: false, }, { name: "wrapped error", err: fmt.Errorf("%w", CommandError{Code: 11000, Name: "blah"}), result: true, }, { name: "other error type", err: errors.New("foo"), result: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { res := IsDuplicateKeyError(tc.err) assert.Equal(t, res, tc.result, "expected IsDuplicateKeyError %v, got %v", tc.result, res) }) } } func TestIsNetworkError(t *testing.T) { const networkLabel = "NetworkError" const otherLabel = "other" testCases := []struct { name string err error result bool }{ { name: "ServerError true", err: CommandError{ Code: 100, Labels: []string{networkLabel}, Name: "blah", }, result: true, }, { name: "ServerError false", err: CommandError{ Code: 100, Labels: []string{otherLabel}, Name: "blah", }, result: false, }, { name: "wrapped error", err: fmt.Errorf("%w", CommandError{ Code: 100, Labels: []string{networkLabel}, Name: "blah", }), result: true, }, { name: "other error type", err: errors.New("foo"), result: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { res := IsNetworkError(tc.err) assert.Equal(t, res, tc.result, "expected IsNetworkError %v, got %v", tc.result, res) }) } } func TestIsTimeout(t *testing.T) { testCases := []struct { name string err error result bool }{ { name: "context timeout", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: context.DeadlineExceeded, Raw: nil, }, result: true, }, { name: "deadline would be exceeded", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: driver.ErrDeadlineWouldBeExceeded, Raw: nil, }, result: true, }, { name: "server selection timeout", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: context.DeadlineExceeded, Raw: nil, }, result: true, }, { name: "wait queue timeout", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: topology.WaitQueueTimeoutError{}, Raw: nil, }, result: true, }, { name: "ServerError NetworkTimeoutError", err: CommandError{ Code: 100, Message: "", Labels: []string{"NetworkTimeoutError"}, Name: "blah", Wrapped: nil, Raw: nil, }, result: true, }, { name: "ServerError ExceededTimeLimitError", err: CommandError{ Code: 100, Message: "", Labels: []string{"ExceededTimeLimitError"}, Name: "blah", Wrapped: nil, Raw: nil, }, result: true, }, { name: "ServerError false", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: nil, Raw: nil, }, result: false, }, { name: "net error true", err: CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: netErr{true}, Raw: nil, }, result: true, }, { name: "net error false", err: netErr{false}, result: false, }, { name: "wrapped error", err: fmt.Errorf("%w", CommandError{ Code: 100, Message: "", Labels: []string{"other"}, Name: "blah", Wrapped: context.DeadlineExceeded, Raw: nil, }), result: true, }, { name: "other error", err: errors.New("foo"), result: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { res := IsTimeout(tc.err) assert.Equal(t, res, tc.result, "expected IsTimeout %v, got %v", tc.result, res) }) } } func TestServerError_ErrorCodes(t *testing.T) { tests := []struct { name string error ServerError want []int }{ { name: "CommandError", error: CommandError{Code: 1}, want: []int{1}, }, { name: "WriteError", error: WriteError{Code: 1}, want: []int{1}, }, { name: "WriteException single", error: WriteException{WriteErrors: []WriteError{{Code: 1}}}, want: []int{1}, }, { name: "WriteException multiple", error: WriteException{WriteErrors: []WriteError{{Code: 1}, {Code: 2}}}, want: []int{1, 2}, }, { name: "WriteException duplicates", error: WriteException{WriteErrors: []WriteError{{Code: 1}, {Code: 2}, {Code: 2}}}, want: []int{1, 2, 2}, }, { name: "BulkWriteException single", error: BulkWriteException{WriteErrors: []BulkWriteError{{WriteError: WriteError{Code: 1}}}}, want: []int{1}, }, { name: "BulkWriteException multiple", error: BulkWriteException{WriteErrors: []BulkWriteError{ {WriteError: WriteError{Code: 1}}, {WriteError: WriteError{Code: 2}}, }}, want: []int{1, 2}, }, { name: "BulkWriteException duplicates", error: BulkWriteException{WriteErrors: []BulkWriteError{ {WriteError: WriteError{Code: 1}}, {WriteError: WriteError{Code: 2}}, {WriteError: WriteError{Code: 2}}, }}, want: []int{1, 2, 2}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := test.error.ErrorCodes() assert.ElementsMatch(t, got, test.want) }) } } type netErr struct { timeout bool } func (n netErr) Error() string { return "error" } func (n netErr) Timeout() bool { return n.timeout } func (n netErr) Temporary() bool { return false } var _ net.Error = (*netErr)(nil) func TestErrorCodes(t *testing.T) { tests := []struct { name string input error want []int }{ { name: "nil error", input: nil, want: nil, }, { name: "non-server error", input: errors.New("boom"), want: []int{}, }, { name: "CommandError single code", input: CommandError{Code: 1}, want: []int{1}, }, { name: "WriteError single code", input: WriteError{Code: 1}, want: []int{1}, }, { name: "WriteException write errors only", input: WriteException{WriteErrors: WriteErrors{{Code: 1}, {Code: 2}}}, want: []int{1, 2}, }, { name: "WriteException with write concern error", input: WriteException{WriteErrors: WriteErrors{{Code: 1}}, WriteConcernError: &WriteConcernError{Code: 2}}, want: []int{1, 2}, }, { name: "BulkWriteException write errors only", input: BulkWriteException{ WriteErrors: []BulkWriteError{ {WriteError: WriteError{Code: 1}}, {WriteError: WriteError{Code: 2}}, }, }, want: []int{1, 2}, }, { name: "BulkWriteException with write concern error", input: BulkWriteException{ WriteErrors: []BulkWriteError{ {WriteError: WriteError{Code: 1}}, {WriteError: WriteError{Code: 2}}, }, WriteConcernError: &WriteConcernError{Code: 3}, }, want: []int{1, 2, 3}, }, { name: "driver.Error wraps to CommandError", input: driver.Error{Code: 1, Message: "shutdown in progress"}, want: []int{1}, }, { name: "wrapped driver.Error", input: fmt.Errorf("context: %w", driver.Error{Code: 1, Message: "ExceededTimeLimit"}), want: []int{1}, }, { input: wrapErrors(driver.Error{Code: 1, Message: "Custom error"}), name: "double wrapped driver.Error", want: []int{1}, }, { name: "already wrapped CommandError", input: CommandError{Code: 1}, want: []int{1}, }, { name: "CommandError wrapped in fmt.Errorf", input: fmt.Errorf("operation failed: %w", CommandError{Code: 1}), want: []int{1}, }, { name: "WriteException wrapped in fmt.Errorf", input: fmt.Errorf("batch failed: %w", WriteException{ WriteErrors: WriteErrors{{Code: 1}, {Code: 2}}, }), want: []int{1, 2}, }, { name: "BulkWriteException with all error types", input: BulkWriteException{ WriteErrors: []BulkWriteError{ {WriteError: WriteError{Code: 1}}, {WriteError: WriteError{Code: 2}}, {WriteError: WriteError{Code: 1}}, }, WriteConcernError: &WriteConcernError{Code: 2}, }, want: []int{1, 2, 1, 2}, }, { name: "driver.Error with multiple fields", input: driver.Error{Code: 1, Message: "test", Name: "TestError", Labels: []string{"label1"}}, want: []int{1}, }, { name: "topology.ErrTopologyClosed converts to ErrClientDisconnected", input: topology.ErrTopologyClosed, want: []int{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, ErrorCodes(tt.input)) }) } } func TestErrorCodesNoDoubleWrapping(t *testing.T) { driverErr := driver.Error{Code: 1, Message: "test error"} // Wrap it once wrapped := wrapErrors(driverErr) cmdErr, ok := wrapped.(CommandError) require.True(t, ok, "wrapErrors should return CommandError") require.Equal(t, int32(1), cmdErr.Code) // Call ErrorCodes on the wrapped error codes := ErrorCodes(wrapped) require.Equal(t, []int{1}, codes) // The wrapped error's structure should not have changed cmdErrAfter, ok := wrapped.(CommandError) require.True(t, ok, "error should still be CommandError") require.Equal(t, cmdErr.Code, cmdErrAfter.Code) // Verify that calling ErrorCodes again gives same result codes2 := ErrorCodes(wrapped) require.Equal(t, codes, codes2) }