From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

# Copyright 2018 - present MongoDB, Inc.
#
# 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
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
use strict;
use MongoDB;
use lib "t/lib";
qw/skip_unless_mongod build_client get_test_db server_version server_type/;
skip_unless_mongod();
my $conn = build_client();
my $server_version = server_version($conn);
my $server_type = server_type($conn);
#--------------------------------------------------------------------------#
# Event callback for testing -- just closures over an array
#--------------------------------------------------------------------------#
my @events;
sub clear_events { @events = () }
sub event_count { scalar @events }
sub event_cb { push @events, $_[0] }
#--------------------------------------------------------------------------#
# Tests
#--------------------------------------------------------------------------#
subtest "Initialize client with monitoring callback" => sub {
my $mc = build_client( monitoring_callback => \&event_cb );
clear_events();
$mc->monitoring_callback->( { hello => "world" } );
is( event_count(), 1, "got events" );
is( $events[0]->{hello}, "world", "correct event" );
};
subtest "run_command" => sub {
clear_events();
my $mc = build_client( monitoring_callback => \&event_cb, dt_type => undef );
$mc->send_admin_command( [ ismaster => 1 ] );
ok( event_count() >= 2, "got 2+ events" ) or return;
subtest "command_started" => sub {
my @started = grep { ($_->{type} // '') eq "command_started" } @events;
ok( scalar @started >= 1, "command_success count" ) or return;
# last command should be the one we ran
my $last_start = $started[-1];
my $ok = 1;
$ok &&= is( $last_start->{databaseName}, "admin", "databaseName" );
$ok &&= is( $last_start->{commandName}, "ismaster", "commandName" );
$ok &&= is( $last_start->{command}{ismaster}, 1, "command" );
$ok &&= ok( defined $last_start->{requestId}, "requestId" );
$ok &&= like( $last_start->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
diag explain $last_start unless $ok;
};
subtest "command_succeeded" => sub {
my @success = grep { ($_->{type} // '') eq "command_succeeded" } @events;
ok( scalar @success >= 1, "command_succeeded count" ) or return;
# last command should be the one we ran
my $last_success = $success[-1];
my $ok = 1;
$ok &&= is( $last_success->{databaseName}, "admin", "databaseName" );
$ok &&= is( $last_success->{commandName}, "ismaster", "commandName" );
$ok &&= ok( defined $last_success->{requestId}, "requestId" );
$ok &&= ok( $last_success->{durationSecs} > 0, "duration" );
$ok &&= like( $last_success->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
diag explain $last_success unless $ok;
};
subtest "command_failed" => sub {
clear_events();
eval { $mc->send_admin_command( [ notarealcommand => 1 ] ) };
ok( $@, "Got exception" );
ok( event_count() >= 2, "got 2+ events" ) or return;
my @failure = grep { ($_->{type} // '') eq "command_failed" } @events;
ok( scalar @failure >= 1, "command_failed count" ) or return;
# last command should be the one we ran
my $last_failure = $failure[-1];
my $ok = 1;
$ok &&= is( $last_failure->{databaseName}, "admin", "databaseName" );
$ok &&= is( $last_failure->{commandName}, "notarealcommand", "commandName" );
$ok &&= ok( defined $last_failure->{requestId}, "requestId" );
$ok &&= ok( $last_failure->{durationSecs} > 0, "duration" );
$ok &&= like( $last_failure->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
$ok &&= like( $last_failure->{failure}, qr/no such (?:command|cmd)|command not found/i, "failure" );
$ok &&= isa_ok( $last_failure->{reply}, 'HASH', "reply");
diag explain $last_failure unless $ok;
};
};
subtest "write commands" => sub {
clear_events();
my $coll = _coll_with_monitor( "test_write_events" );
_test_writes($coll);
};
subtest "unack'd writes" => sub {
clear_events();
my $coll = _coll_with_monitor( "test_write_events", { write_concern => { w => 0 } } );
_test_writes($coll);
};
subtest "find and getMore" => sub {
clear_events();
my $coll = _coll_with_monitor("test_read_events");
$coll->insert_many( [ map { ; { x => $_ } } 1 .. 100 ] );
# Clear after insert so we're only looking for find/getmore
clear_events();
my @docs = $coll->find( { x => { '$gt' => 10 } }, { batchSize => 30 } )->all;
subtest "command_started" => sub {
my @started = grep { ($_->{type} // '') eq "command_started" } @events;
ok( scalar @started >= 2, "got events" );
my $ok = 1;
$ok &&= is( (scalar grep { ($_->{commandName} // '') eq 'find' } @started), 1, "find command" );
$ok &&= is( (scalar grep { ($_->{commandName} // '') eq 'getMore' } @started), 3, "getMore commands" );
diag explain \@started unless $ok;
};
subtest "command_succeeded" => sub {
my @succeeded = grep { ($_->{type} // '') eq "command_succeeded" } @events;
ok( scalar @succeeded >= 2, "got events" );
my $ok = 1;
$ok &&= is( (scalar grep { ($_->{commandName} // '') eq 'find' } @succeeded), 1, "find command" );
$ok &&= is( (scalar grep { ($_->{commandName} // '') eq 'getMore' } @succeeded), 3, "getMore commands" );
diag explain \@succeeded unless $ok;
};
subtest "command_failed" => sub {
clear_events();
eval { $coll->find( { x => { '$xxxx' => 10 } }, { batchSize => 30 } )->all };
ok( $@, "Got exception" );
my @failed = grep { ($_->{type} // '') eq "command_failed" } @events;
ok( scalar @failed >= 1, "got events" );
my $ok = 1;
$ok &&= is( (scalar grep { ($_->{commandName} // '') eq 'find' } @failed), 1, "find command" );
diag explain \@failed unless $ok;
};
};
subtest "exceptions are command_failed" => sub {
subtest 'insert' => sub {
no warnings 'redefine';
my $coll = _coll_with_monitor("test");
$coll->insert_one({}); # force topology discovery
my $err;
{
local *MongoDB::_Link::write = \&_throw_mock_network_error;
clear_events();
eval {$coll->insert_one({})};
$err = $@;
}
# force reset topology status
$coll->client->topology_status( refresh => 1 );
ok( $err, "got exception" );
my @failed = grep { ($_->{type} // '') eq "command_failed" } @events;
ok( scalar @failed >= 1, "got events" );
my $last_failure = $failed[-1];
my $ok = 1;
$ok &&= is( $last_failure->{commandName}, "insert", "commandName" );
$ok &&= ok( defined $last_failure->{requestId}, "requestId" );
$ok &&= ok( $last_failure->{durationSecs} > 0, "duration" );
$ok &&= like( $last_failure->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
$ok &&= like( $last_failure->{failure}, qr/fake network error/, "failure msg" );
$ok &&= isa_ok( $last_failure->{eval_error}, "MongoDB::NetworkError", "eval_error" );
diag explain $last_failure unless $ok;
};
subtest "insert unack'd" => sub {
no warnings 'redefine';
my $coll = _coll_with_monitor("test", { write_concern => { w => 0 } });
$coll->insert_one({}); # force topology discovery
my $err;
{
local *MongoDB::_Link::write = \&_throw_mock_network_error;
clear_events();
eval {$coll->insert_one({})};
$err = $@;
}
# force reset topology status
$coll->client->topology_status( refresh => 1 );
ok( $err, "got exception" );
my @failed = grep { ($_->{type} // '') eq "command_failed" } @events;
ok( scalar @failed >= 1, "got events" );
my $last_failure = $failed[-1];
my $ok = 1;
$ok &&= is( $last_failure->{commandName}, "insert", "commandName" );
$ok &&= ok( defined $last_failure->{requestId}, "requestId" );
$ok &&= ok( $last_failure->{durationSecs} > 0, "duration" );
$ok &&= like( $last_failure->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
$ok &&= like( $last_failure->{failure}, qr/fake network error/, "failure msg" );
$ok &&= isa_ok( $last_failure->{eval_error}, "MongoDB::NetworkError", "eval_error" );
diag explain $last_failure unless $ok;
};
subtest 'find' => sub {
no warnings 'redefine';
my $coll = _coll_with_monitor("test");
$coll->insert_one({}); # force topology discovery
my $err;
{
local *MongoDB::_Link::write = \&_throw_mock_network_error;
clear_events();
eval {$coll->find({})->all};
$err = $@;
}
# force reset topology status
$coll->client->topology_status( refresh => 1 );
ok( $err, "got exception" );
my @failed =
grep { $_->{'commandName'} !~ /sasl|ismaster/ }
grep { ($_->{type} // '') eq "command_failed" } @events;
ok( scalar @failed >= 1, "got events" );
my $last_failure = $failed[-1];
my $ok = 1;
$ok &&= is( $last_failure->{commandName}, "find", "commandName" );
$ok &&= ok( defined $last_failure->{requestId}, "requestId" );
$ok &&= ok( $last_failure->{durationSecs} > 0, "duration" );
$ok &&= like( $last_failure->{connectionId}, qr/^[^:]+:\d+$/, "connectionId" );
$ok &&= like( $last_failure->{failure}, qr/fake network error/, "failure msg" );
$ok &&= isa_ok( $last_failure->{eval_error}, "MongoDB::NetworkError", "eval_error" );
diag explain $last_failure unless $ok;
};
};
subtest 'redactions' => sub {
clear_events();
my $mc = build_client( monitoring_callback => \&event_cb );
my $testdb = get_test_db($mc);
$testdb->run_command([getnonce => 1]);
my ($started, $succeeded) =
grep { ($_->{commandName} // '') eq 'getnonce' }
@events;
is $started->{type}, 'command_started', 'start event';
is $succeeded->{type}, 'command_succeeded', 'success event';
ok defined($started->{command}), 'command not empty';
ok defined($succeeded->{reply}), 'reply not empty';
is scalar(keys %{ $started->{command} }), 0, 'no command fields';
is scalar(keys %{ $succeeded->{reply} }), 0, 'no reply fields';
};
sub _coll_with_monitor {
# retry writes messes with event counting and iterating over events from
# monitoring callback
my $mc = build_client( monitoring_callback => \&event_cb, retry_writes => 0, retry_reads => 0 );
my $testdb = get_test_db($mc);
my $col = $testdb->coll(@_);
}
sub _throw_mock_network_error {
MongoDB::NetworkError->throw("fake network error");
}
sub _test_writes {
my ($coll) = shift;
local $Test::Builder::Level = $Test::Builder::Level + 1;
$coll->insert_one( { x => 1 } );
$coll->replace_one( { x => 1 }, { x => 0 } );
$coll->delete_one( { x => 0 } );
subtest "command_started" => sub {
my @started = grep { ($_->{type} // '') eq "command_started" } @events;
ok( scalar @started >= 3, "got events" ) or return;
my $ok = 1;
for my $cmd (qw/insert update delete/) {
$ok &&=
ok( ( scalar grep { ($_->{commandName} // '') eq $cmd } @started ), "saw $cmd command" );
}
diag explain \@started unless $ok;
};
subtest "command_succeeded" => sub {
my @succeeded = grep { ($_->{type} // '') eq "command_succeeded" } @events;
ok( scalar @succeeded >= 3, "got events" ) or return;
my $ok = 1;
for my $cmd (qw/insert update delete/) {
$ok &&=
ok( ( scalar grep { ($_->{commandName} // '') eq $cmd } @succeeded ), "saw $cmd command" );
}
diag explain \@succeeded unless $ok;
};
subtest "failed write is still command_succeeded" => sub {
plan skip_all => "w:0 won't error"
unless $coll->write_concern->is_acknowledged;
$coll->insert_one( { _id => 123 } );
clear_events();
eval { $coll->insert_one( { _id => 123 } ) };
ok( $@, "Got exception" );
my @succeeded = grep { ($_->{type} // '') eq "command_succeeded" } @events;
ok( scalar @succeeded >= 1, "got events" ) or return;
ok( ( scalar grep { ($_->{commandName} // '') eq 'insert' } @succeeded ), "saw insert command" )
or diag explain \@succeeded;
};
}
done_testing;