<commit_before>use Future::Utils qw( try_repeat_until_success );
#use Devel::StackTrace;

push our @EXPORT, qw( is_user_in_changed_list sync_until_user_in_device_list
                      sync_until_user_in_device_list_id );

sub is_user_in_changed_list
{
   my ( $user, $body ) = @_;

   return $body->{device_lists} &&
          $body->{device_lists}{changed} &&
          any { $_ eq $user->user_id } @{ $body->{device_lists}{changed} };
}


sub sync_until_user_in_device_list
{
   my ( $syncing_user, $user_to_wait_for, %params ) = @_;
   sync_until_user_in_device_list_id($syncing_user, $user_to_wait_for->user_id, %params)
}

# returns a Future which resolves to the body of the sync result which contains
# the change notification
sub sync_until_user_in_device_list_id
{
   my ( $syncing_user, $wait_for_id, %params ) = @_;

   my $device_list = $params{device_list} // 'changed';
   my $msg = $params{msg} // 'sync_until_user_in_device_list';

   # my $trace = Devel::StackTrace->new(no_args => 1);
   # log_if_fail $trace->frame(1)->as_string();

   log_if_fail "$msg: waiting for $wait_for_id in $device_list";

   return await_sync( $syncing_user, 
      update_next_batch => 1,
      check => sub {
         my ( $body ) = @_;
         log_if_fail "$msg: body", $body;

         return unless
            $body->{device_lists} &&
            $body->{device_lists}{$device_list} &&
            any { $_ eq $wait_for_id } @{ $body->{device_lists}{$device_list} };

         log_if_fail "$msg: found $wait_for_id in $device_list";
         return $body;
      },
   )
}


test "Local device key changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         do_request_json_for( $user2,
            method  => "POST",
            uri     => "/v3/keys/upload",
            content => {
               device_keys => {
                  user_id   => $user2->user_id,
                  device_id => $user2->device_id,
               },
               one_time_keys => {
                  "my_algorithm:my_id_1" => "my+base64+key"
               }
            }
         )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };


test "Local new device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my ( $room_id );

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_login_again_with_user( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };

test "Local delete device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_delete_device( $user2, $user2->device_id, {
             auth => {
                 type     => "m.login.password",
                 identifier => {
                    type => "m.id.user",
                    user => $user2->user_id,
                 },
                 password => $<PASSWORD>,
             }
         });
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };

test "Local update device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my ( $room_id );

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "wibble");
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };


test "Can query remote device keys using POST after notification",
   requires => [ local_user_fixture(), remote_user_fixture(),
                 qw( can_upload_e2e_keys )],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_invite_user_to_room_synced( $user1, $user2, $room_id )
      })->then( sub {
         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_put_e2e_keys( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "test display name" ),
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_get_e2e_keys(
            $user1, $user2->user_id
         )
      })->then( sub {
         my ( $content ) = @_;

         log_if_fail "key query content", $content;

         assert_json_keys( $content, "device_keys" );

         my $device_keys = $content->{device_keys};
         assert_json_keys( $device_keys, $user2->user_id );

         my $alice_keys = $device_keys->{ $user2->user_id };
         assert_json_keys( $alice_keys, $user2->device_id );

         my $alice_device_keys = $alice_keys->{ $user2->device_id };

         # TODO: Check that the content matches what we uploaded.

         assert_eq( $alice_device_keys->{"unsigned"}->{"device_display_name"},
                    "test display name" );

         Future->done(1)
      });
   };


# regression test for https://github.com/vector-im/riot-web/issues/4527
test "Device deletion propagates over federation",
   requires => [ local_user_fixture(), remote_user_fixture(),
                 qw( can_upload_e2e_keys )],


   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_invite_user_to_room_synced( $user1, $user2, $room_id )
      })->then( sub {
         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_put_e2e_keys( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "test display name" ),<commit_msg>Allow HSes to omit device display names (#1307)<commit_after>use Future::Utils qw( try_repeat_until_success );
#use Devel::StackTrace;

push our @EXPORT, qw( is_user_in_changed_list sync_until_user_in_device_list
                      sync_until_user_in_device_list_id );

sub is_user_in_changed_list
{
   my ( $user, $body ) = @_;

   return $body->{device_lists} &&
          $body->{device_lists}{changed} &&
          any { $_ eq $user->user_id } @{ $body->{device_lists}{changed} };
}


sub sync_until_user_in_device_list
{
   my ( $syncing_user, $user_to_wait_for, %params ) = @_;
   sync_until_user_in_device_list_id($syncing_user, $user_to_wait_for->user_id, %params)
}

# returns a Future which resolves to the body of the sync result which contains
# the change notification
sub sync_until_user_in_device_list_id
{
   my ( $syncing_user, $wait_for_id, %params ) = @_;

   my $device_list = $params{device_list} // 'changed';
   my $msg = $params{msg} // 'sync_until_user_in_device_list';

   # my $trace = Devel::StackTrace->new(no_args => 1);
   # log_if_fail $trace->frame(1)->as_string();

   log_if_fail "$msg: waiting for $wait_for_id in $device_list";

   return await_sync( $syncing_user, 
      update_next_batch => 1,
      check => sub {
         my ( $body ) = @_;
         log_if_fail "$msg: body", $body;

         return unless
            $body->{device_lists} &&
            $body->{device_lists}{$device_list} &&
            any { $_ eq $wait_for_id } @{ $body->{device_lists}{$device_list} };

         log_if_fail "$msg: found $wait_for_id in $device_list";
         return $body;
      },
   )
}


test "Local device key changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         do_request_json_for( $user2,
            method  => "POST",
            uri     => "/v3/keys/upload",
            content => {
               device_keys => {
                  user_id   => $user2->user_id,
                  device_id => $user2->device_id,
               },
               one_time_keys => {
                  "my_algorithm:my_id_1" => "my+base64+key"
               }
            }
         )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };


test "Local new device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my ( $room_id );

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_login_again_with_user( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };

test "Local delete device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_delete_device( $user2, $user2->device_id, {
             auth => {
                 type     => "m.login.password",
                 identifier => {
                    type => "m.id.user",
                    user => $user2->user_id,
                 },
                 password => <PASSWORD>,
             }
         });
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };

test "Local update device changes appear in v2 /sync",
   requires => [ local_user_fixtures( 2 ),
                 qw( can_sync ) ],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my ( $room_id );

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "wibble");
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      });
   };


test "Can query remote device keys using POST after notification",
   requires => [ local_user_fixture(), remote_user_fixture(),
                 qw( can_upload_e2e_keys )],

   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_invite_user_to_room_synced( $user1, $user2, $room_id )
      })->then( sub {
         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_put_e2e_keys( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "test display name" ),
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_get_e2e_keys(
            $user1, $user2->user_id
         )
      })->then( sub {
         my ( $content ) = @_;

         log_if_fail "key query content", $content;

         assert_json_keys( $content, "device_keys" );

         my $device_keys = $content->{device_keys};
         assert_json_keys( $device_keys, $user2->user_id );

         my $alice_keys = $device_keys->{ $user2->user_id };
         assert_json_keys( $alice_keys, $user2->device_id );

         my $alice_device_keys = $alice_keys->{ $user2->device_id };

         # TODO: Check that the content matches what we uploaded.
         # Device display names aren't mandated in the POST /user/keys/query response,
         # and they're considered optional in the GET /user/devices/{userId} response.
         # So accept either a match or a lack of key.
         my $device_display_name = $alice_device_keys->{"unsigned"}->{"device_display_name"};
         (!defined $device_display_name) or ($device_display_name == "test display name") or
            croak "Unexpected device_display_name: $device_display_name";

         Future->done(1)
      });
   };


# regression test for https://github.com/vector-im/riot-web/issues/4527
test "Device deletion propagates over federation",
   requires => [ local_user_fixture(), remote_user_fixture(),
                 qw( can_upload_e2e_keys )],


   check => sub {
      my ( $user1, $user2 ) = @_;

      my $room_id;

      matrix_create_room_synced( $user1 )->then( sub {
         ( $room_id ) = @_;

         matrix_invite_user_to_room_synced( $user1, $user2, $room_id )
      })->then( sub {
         matrix_join_room_synced( $user2, $room_id );
      })->then( sub {
         matrix_sync( $user1 );
      })->then( sub {
         matrix_put_e2e_keys( $user2 )
      })->then( sub {
         sync_until_user_in_device_list( $user1, $user2 );
      })->then( sub {
         matrix_set_device_display_name( $user2, $user2->device_id, "test display name" ),
{'fraction_non_alphanumeric': 0.13932229019176576, 'fraction_numerical': 0.013196783284074507, 'mean_word_length': 2.1466262975778547, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 183, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1448455', 'n_tokens_mistral': 5025, 'n_tokens_neox': 4883, 'n_words': 984}