diff --git a/lib/SyTest/Assertions.pm b/lib/SyTest/Assertions.pm index d30b7ddaa..9ba16881d 100644 --- a/lib/SyTest/Assertions.pm +++ b/lib/SyTest/Assertions.pm @@ -120,8 +120,8 @@ sub _assert_deeply_eq croak "Got a value for '$key' that was not expected at $outerkeystr for $name"; } } - elsif( $wanttype eq JSON_BOOLEAN_CLASS ) { - ref $got eq JSON_BOOLEAN_CLASS and $got eq $want or + elsif( $wanttype eq JSON_BOOLEAN_CLASS or $wanttype eq "JSON::number" ) { + $got eq $want or croak "Got ${\ pp $got }, expected ${\ pp $want } at $outerkeystr for $name"; } else { diff --git a/tests/00expect_http_fail.pl b/tests/00expect_http_fail.pl index afc26c606..2e7ccdf7c 100644 --- a/tests/00expect_http_fail.pl +++ b/tests/00expect_http_fail.pl @@ -114,18 +114,61 @@ sub check_http_code ); } +=head2 expect_matrix_error -# todo: generalise this to other http errors/matrix errcodes -sub expect_m_not_found + http_request()->main::expect_matrix_error( + $http_code, $matrix_errcode, + )->then( sub { + my ( $error_body ) = @_; + }); + +A decorator for a Future which we expect to return an HTTP error with a Matrix +error code. Asserts that the HTTP error code and matrix error code were as +expected, and if so returns the JSON-decoded body of the error (so that it can +be checked for additional fields). + +=cut + +sub expect_matrix_error { - my $f = shift; + my ( $f, $expected_http_code, $expected_errcode ) = @_; + + return $f->then_with_f( + sub { # done + my ( undef, $response ) = @_; + + log_if_fail "Response", $response; + Future->fail( + "Expected to receive an HTTP $expected_http_code failure but it succeeded" + ); + }, + http => sub { # catch http + my ( $f, undef, undef, $response ) = @_; - $f->main::expect_http_404() - ->then( sub { + $response and $response->code == $expected_http_code and + return Future->done( $response ); + + # if the error code doesn't match, return the failure. + return $f; + }, + )->then( sub { my ( $response ) = @_; my $body = decode_json( $response->content ); - assert_eq( $body->{errcode}, "M_NOT_FOUND", 'responsecode' ); - Future->done( 1 ); + + log_if_fail "Error response body", $body; + + assert_eq( $body->{errcode}, $expected_errcode, 'errcode' ); + Future->done( $body ); }); } -push @EXPORT, qw/expect_m_not_found/; +push @EXPORT, qw( expect_matrix_error ); + + +sub expect_m_not_found +{ + my $f = shift; + return expect_matrix_error( + $f, 404, 'M_NOT_FOUND', + ); +} +push @EXPORT, qw( expect_m_not_found ); diff --git a/tests/30rooms/60version_upgrade.pl b/tests/30rooms/60version_upgrade.pl new file mode 100644 index 000000000..46af5514a --- /dev/null +++ b/tests/30rooms/60version_upgrade.pl @@ -0,0 +1,468 @@ +# Copyright 2018 New Vector Ltd +# +# 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 +# +# 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 Future::Utils qw( repeat ); +use List::Util qw( first ); + +# TODO: switch this to '2' once that is released +my $TEST_NEW_VERSION = 'vdh-test-version'; + +=head2 upgrade_room + + upgrade_room( $user, $room_id, %opts )->then( sub { + my ( $new_room_id ) = @_; + }) + +Request that the homeserver upgrades the given room. + +%opts may include: + +=over + +=item new_version => STRING + +Defaults to $TEST_NEW_VERSION if unspecified + +=back + +=cut + +sub upgrade_room { + my ( $user, $room_id, %opts ) = @_; + + my $new_version = $opts{new_version} // $TEST_NEW_VERSION; + + do_request_json_for( + $user, + method => "POST", + uri => "/r0/rooms/$room_id/upgrade", + content => { + new_version => $new_version, + }, + )->then( sub { + my ( $body ) = @_; + log_if_fail "upgrade response", $body; + + assert_json_keys( $body, qw( replacement_room ) ); + Future->done( $body->{replacement_room} ); + }); +} + +=head2 upgrade_room_synced + + upgrade_room_synced( $user, $room_id, %opts )->then( sub { + my ( $new_room_id ) = @_; + }) + +Request that the homeserver upgrades the given room, and waits for the +new room to appear in the sync result. + +%opts may include: + +=over + +=item expected_event_counts => HASH + +The number of events of each type we expect to appear in the new room. A map +from event type to count. + +=back + +Other %opts are as for C. + +=cut + +sub upgrade_room_synced { + my ( $user, $room_id, %opts ) = @_; + + my $expected_event_counts = delete $opts{expected_event_counts} // {}; + foreach my $t (qw( + m.room.create m.room.member m.room.guest_access + m.room.history_visibility m.room.join_rules m.room.power_levels + )) { + $expected_event_counts->{$t} //= 1; + } + + # map from event type to count + my %received_event_counts = (); + + matrix_do_and_wait_for_sync( + $user, + do => sub { + upgrade_room( $user, $room_id, %opts ); + }, + check => sub { + my ( $sync_body, $new_room_id ) = @_; + return 0 if not exists $sync_body->{rooms}{join}{$new_room_id}; + my $tl = $sync_body->{rooms}{join}{$new_room_id}{timeline}{events}; + log_if_fail "New room timeline", $tl; + + foreach my $ev ( @$tl ) { + $received_event_counts{$ev->{type}} //= 0; + $received_event_counts{$ev->{type}} += 1; + } + + # check we've got all the events we expect + foreach my $t ( keys %$expected_event_counts ) { + if( $received_event_counts{$t} < $expected_event_counts->{$t} ) { + log_if_fail "Still waiting for a $t event"; + return 0; + } + } + return 1; + }, + ); +} + +test "/upgrade creates a new room", + requires => [ + local_user_and_room_fixtures(), + qw( can_create_versioned_room ), + ], + + proves => [ qw( can_upgrade_room_version ) ], + + do => sub { + my ( $user, $old_room_id ) = @_; + my ( $new_room_id ); + + matrix_sync( $user )->then( sub { + upgrade_room_synced( + $user, $old_room_id, + new_version => $TEST_NEW_VERSION, + ); + })->then( sub { + ( $new_room_id, ) = @_; + + matrix_sync_again( $user ); + })->then( sub { + my ( $sync_body ) = @_; + + log_if_fail "sync body", $sync_body; + + # check the new room has the right version + + my $room = $sync_body->{rooms}{join}{$new_room_id}; + my $ev0 = $room->{timeline}{events}[0]; + + assert_eq( $ev0->{type}, 'm.room.create', 'first event in new room' ); + assert_json_keys( $ev0->{content}, qw( room_version )); + assert_eq( $ev0->{content}{room_version}, $TEST_NEW_VERSION, 'room_version' ); + + # the old room should have a tombstone event + my $old_room_timeline = $sync_body->{rooms}{join}{$old_room_id}{timeline}{events}; + my $tombstone_event = $old_room_timeline->[0]; + assert_eq( + $tombstone_event->{type}, + 'm.room.tombstone', + 'event in old room', + ); + assert_eq( + $tombstone_event->{content}{replacement_room}, + $new_room_id, + 'room_id in tombstone' + ); + + # the new room should link to the old room + assert_json_keys( $ev0->{content}, qw( predecessor )); + assert_json_keys( $ev0->{content}{predecessor}, qw( room_id event_id )); + assert_eq( $ev0->{content}{predecessor}{room_id}, $old_room_id ); + assert_eq( $ev0->{content}{predecessor}{event_id}, $tombstone_event->{event_id} ); + + Future->done(1); + }); + }; + +foreach my $vis ( qw( public private ) ) { + test "/upgrade should preserve room visibility for $vis rooms", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version ), + ], + + do => sub { + my ( $creator, $room_id ) = @_; + + # set the visibility on the old room. (The default is 'private', but + # we may as well set it explicitly.) + do_request_json_for( + $creator, + method => "PUT", + uri => "/r0/directory/list/room/$room_id", + content => { + visibility => $vis, + }, + )->then( sub { + upgrade_room_synced( + $creator, $room_id, + new_version => $TEST_NEW_VERSION, + ); + })->then( sub { + my ( $new_room_id, ) = @_; + + # check the visibility of the new room + do_request_json_for( + $creator, + method => "GET", + uri => "/r0/directory/list/room/$new_room_id", + ); + })->then( sub { + my ( $response ) = @_; + log_if_fail "room vis", $response; + assert_eq( + $response->{visibility}, + $vis, + "replacement room visibility", + ); + Future->done(1); + }); + }; +} + +test "/upgrade copies the power levels to the new room", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version can_change_power_levels ), + ], + + do => sub { + my ( $creator, $room_id ) = @_; + + my ( $pl_content, $new_room_id ); + + matrix_change_room_power_levels( + $creator, $room_id, sub { + ( $pl_content ) = @_; + $pl_content->{users}->{'@test:xyz'} = 40; + log_if_fail "PL content in old room", $pl_content; + } + )->then( sub { + matrix_sync( $creator ); + })->then( sub { + upgrade_room_synced( + $creator, $room_id, + expected_event_counts => { 'm.room.power_levels' => 2 }, + new_version => $TEST_NEW_VERSION, + ); + })->then( sub { + ( $new_room_id, ) = @_; + + matrix_sync_again( $creator ); + })->then( sub { + my ( $sync_body ) = @_; + + log_if_fail "sync body", $sync_body; + + my $room = $sync_body->{rooms}{join}{$new_room_id}; + my $pl_event = first { + $_->{type} eq 'm.room.power_levels' + } reverse @{ $room->{timeline}->{events} }; + + log_if_fail "PL event in new room", $pl_event; + + assert_deeply_eq( + $pl_event->{content}, + $pl_content, + "power levels in replacement room", + ); + Future->done(1); + }); + }; + + +test "/upgrade copies important state to the new room", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version ), + ], + + do => sub { + my ( $creator, $room_id ) = @_; + my ( $new_room_id ); + + # map from type to content + my %STATE_DICT = ( + "m.room.topic" => { topic => "topic" }, + "m.room.name" => { name => "name" }, + "m.room.join_rules" => { join_rule => "public" }, + "m.room.guest_access" => { guest_access => "forbidden" }, + "m.room.history_visibility" => { history_visibility => "joined" }, + "m.room.avatar" => { url => "http://something" }, + ); + + my $f = Future->done(1); + foreach my $k ( keys %STATE_DICT ) { + $f = $f->then( sub { + matrix_put_room_state( + $creator, $room_id, + type => $k, + content => $STATE_DICT{$k}, + ); + }); + } + + $f->then( sub { + matrix_sync( $creator ); + })->then( sub { + upgrade_room_synced( + $creator, $room_id, + new_version => $TEST_NEW_VERSION, + ); + })->then( sub { + ( $new_room_id, ) = @_; + + matrix_sync_again( $creator ); + })->then( sub { + my ( $sync_body ) = @_; + + log_if_fail "sync body", $sync_body; + + my $room = $sync_body->{rooms}{join}{$new_room_id}; + + foreach my $k ( keys %STATE_DICT ) { + my $event = first { + $_->{type} eq $k && $_->{state_key} eq '', + } @{ $room->{timeline}->{events} }; + + log_if_fail "State for $k", $event->{content}; + assert_deeply_eq( + $event->{content}, + $STATE_DICT{$k}, + "$k in replacement room", + ); + } + Future->done(1); + }); + }; + +test "/upgrade restricts power levels in the old room", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version ), + ], + + do => sub { + my ( $creator, $room_id ) = @_; + + log_if_fail "Old room id", $room_id; + + upgrade_room_synced( + $creator, $room_id, + new_version => $TEST_NEW_VERSION, + )->then( sub { + my ( $new_room_id ) = @_; + + matrix_get_room_state( + $creator, $room_id, type=>'m.room.power_levels', + ); + })->then( sub { + my ( $pl_event ) = @_; + + log_if_fail 'power_levels after upgrade', $pl_event; + assert_eq( $pl_event->{events_default}, 50, "events_default" ); + assert_eq( $pl_event->{invite}, 50, "invite" ); + Future->done(1); + }); + }; + +test "/upgrade restricts power levels in the old room when the old PLs are unusual", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version ), + ], + + do => sub { + my ( $creator, $room_id ) = @_; + + matrix_change_room_power_levels( + $creator, $room_id, sub { + my ( $levels ) = @_; + $levels -> {users_default} = 80; + } + )->then( sub { + upgrade_room_synced( + $creator, $room_id, + expected_event_counts => { 'm.room.power_levels' => 2 }, + new_version => $TEST_NEW_VERSION, + ); + })->then( sub { + my ( $new_room_id ) = @_; + + matrix_get_room_state( + $creator, $room_id, type=>'m.room.power_levels', + ); + })->then( sub { + my ( $pl_event ) = @_; + + log_if_fail 'power_levels after upgrade', $pl_event; + + assert_eq( $pl_event->{events_default}, 81, "events_default" ); + assert_eq( $pl_event->{invite}, 81, "invite" ); + Future->done(1); + }); + }; + +test "/upgrade to an unknown version is rejected", + requires => [ + local_user_and_room_fixtures(), + qw( can_upgrade_room_version ), + ], + + do => sub { + my ( $user, $room_id ) = @_; + + upgrade_room( + $user, $room_id, + new_version => 'my_bad_version', + )->main::expect_matrix_error( 400, 'M_UNSUPPORTED_ROOM_VERSION' ); + }; + +test "/upgrade is rejected if the user can't send state events", + requires => [ + local_user_and_room_fixtures(), + local_user_fixture(), + qw( can_create_versioned_room ), + ], + + do => sub { + my ( $creator, $room_id, $joiner ) = @_; + my ( $replacement_room ); + + matrix_join_room( $joiner, $room_id )->then( sub { + upgrade_room( + $joiner, $room_id, + )->main::expect_matrix_error( 403, 'M_FORBIDDEN' ); + }); + }; + +test "/upgrade of a bogus room fails gracefully", + requires => [ + local_user_fixture(), + ], + + do => sub { + my ( $user ) = @_; + my ( $replacement_room ); + + upgrade_room( + $user, "!fail:unknown", + )->main::expect_matrix_error( 404, 'M_NOT_FOUND' ); + }; + +# upgrade without perms +# upgrade with other local users +# upgrade with remote users +# check names and aliases are copied + +