use crate::{ db::{self, UserId}, rpc::RECONNECT_TIMEOUT, tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; use channel::{ChannelId, ChannelMembership, ChannelStore}; use client::User; use futures::future::try_join_all; use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext}; use rpc::{ proto::{self, ChannelRole}, RECEIVE_TIMEOUT, }; use std::sync::Arc; #[gpui::test] async fn test_core_channels( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let channel_a_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("channel-a", None, cx) }) .await .unwrap(); let channel_b_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("channel-b", Some(channel_a_id), cx) }) .await .unwrap(); executor.run_until_parked(); assert_channels( client_a.channel_store(), cx_a, &[ ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".into(), depth: 1, }, ], ); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert!(channels.ordered_channels().collect::>().is_empty()) }) }); // Invite client B to channel A as client A. client_a .channel_store() .update(cx_a, |store, cx| { assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); let invite = store.invite_member( channel_a_id, client_b.user_id().unwrap(), proto::ChannelRole::Member, cx, ); // Make sure we're synchronously storing the pending invite assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); invite }) .await .unwrap(); // Client A sees that B has been invited. executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), cx_b, &[ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }], ); let members = client_a .channel_store() .update(cx_a, |store, cx| { assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap())); store.get_channel_member_details(channel_a_id, cx) }) .await .unwrap(); assert_members_eq( &members, &[ ( client_a.user_id().unwrap(), proto::ChannelRole::Admin, proto::channel_member::Kind::Member, ), ( client_b.user_id().unwrap(), proto::ChannelRole::Member, proto::channel_member::Kind::Invitee, ), ], ); // Client B accepts the invitation. client_b .channel_store() .update(cx_b, |channels, cx| { channels.respond_to_channel_invite(channel_a_id, true, cx) }) .await .unwrap(); executor.run_until_parked(); // Client B now sees that they are a member of channel A and its existing subchannels. assert_channel_invitations(client_b.channel_store(), cx_b, &[]); assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".into(), depth: 1, }, ], ); let channel_c_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("channel-c", Some(channel_b_id), cx) }) .await .unwrap(); executor.run_until_parked(); assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".into(), depth: 1, }, ExpectedChannel { id: channel_c_id, name: "channel-c".into(), depth: 2, }, ], ); // Update client B's membership to channel A to be an admin. client_a .channel_store() .update(cx_a, |store, cx| { store.set_member_role( channel_a_id, client_b.user_id().unwrap(), proto::ChannelRole::Admin, cx, ) }) .await .unwrap(); executor.run_until_parked(); // Observe that client B is now an admin of channel A, and that // their admin privileges extend to subchannels of channel A. assert_channel_invitations(client_b.channel_store(), cx_b, &[]); assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }, ExpectedChannel { id: channel_b_id, name: "channel-b".into(), depth: 1, }, ExpectedChannel { id: channel_c_id, name: "channel-c".into(), depth: 2, }, ], ); // Client A deletes the channel, deletion also deletes subchannels. client_a .channel_store() .update(cx_a, |channel_store, _| { channel_store.remove_channel(channel_b_id) }) .await .unwrap(); executor.run_until_parked(); assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }], ); assert_channels( client_b.channel_store(), cx_b, &[ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }], ); // Remove client B client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.remove_member(channel_a_id, client_b.user_id().unwrap(), cx) }) .await .unwrap(); executor.run_until_parked(); // Client A still has their channel assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { id: channel_a_id, name: "channel-a".into(), depth: 0, }], ); // Client B no longer has access to the channel assert_channels(client_b.channel_store(), cx_b, &[]); server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); server .app_state .db .rename_channel( db::ChannelId::from_proto(channel_a_id), UserId::from_proto(client_a.id()), "channel-a-renamed", ) .await .unwrap(); server.allow_connections(); executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { id: channel_a_id, name: "channel-a-renamed".into(), depth: 0, }], ); } #[track_caller] fn assert_participants_eq(participants: &[Arc], expected_partitipants: &[u64]) { assert_eq!( participants.iter().map(|p| p.id).collect::>(), expected_partitipants ); } #[track_caller] fn assert_members_eq( members: &[ChannelMembership], expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)], ) { assert_eq!( members .iter() .map(|member| (member.user.id, member.role, member.kind)) .collect::>(), expected_members ); } #[gpui::test] async fn test_joining_channel_ancestor_member( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let parent_id = server .make_channel("parent", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; let sub_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("sub_channel", Some(parent_id), cx) }) .await .unwrap(); let active_call_b = cx_b.read(ActiveCall::global); assert!(active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx)) .await .is_ok()); } #[gpui::test] async fn test_channel_room( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; let zed_id = server .make_channel( "zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); active_call_a .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); // Give everyone a chance to observe user A joining executor.run_until_parked(); let room_a = cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap()], ); }) }); assert_channels( client_b.channel_store(), cx_b, &[ExpectedChannel { id: zed_id, name: "zed".into(), depth: 0, }], ); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap()], ); }) }); cx_c.read(|cx| { client_c.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap()], ); }) }); active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); executor.run_until_parked(); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); cx_c.read(|cx| { client_c.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); let room_a = cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: vec![] } ); let room_b = cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: vec![] } ); // Make sure that leaving and rejoining works active_call_a .update(cx_a, |active_call, cx| active_call.hang_up(cx)) .await .unwrap(); executor.run_until_parked(); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_b.user_id().unwrap()], ); }) }); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_b.user_id().unwrap()], ); }) }); cx_c.read(|cx| { client_c.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_b.user_id().unwrap()], ); }) }); active_call_b .update(cx_b, |active_call, cx| active_call.hang_up(cx)) .await .unwrap(); executor.run_until_parked(); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq(channels.channel_participants(zed_id), &[]); }) }); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert_participants_eq(channels.channel_participants(zed_id), &[]); }) }); cx_c.read(|cx| { client_c.channel_store().read_with(cx, |channels, _| { assert_participants_eq(channels.channel_participants(zed_id), &[]); }) }); active_call_a .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); active_call_b .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); executor.run_until_parked(); let room_a = cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone())); cx_a.read(|cx| room_a.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_a, cx_a), RoomParticipants { remote: vec!["user_b".to_string()], pending: vec![] } ); let room_b = cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone())); cx_b.read(|cx| room_b.read_with(cx, |room, _| assert!(room.is_connected()))); assert_eq!( room_participants(&room_b, cx_b), RoomParticipants { remote: vec!["user_a".to_string()], pending: vec![] } ); } #[gpui::test] async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppContext) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let zed_id = server .make_channel("zed", None, (&client_a, cx_a), &mut []) .await; let rust_id = server .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; let active_call_a = cx_a.read(ActiveCall::global); active_call_a .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx)) .await .unwrap(); // Give everything a chance to observe user A joining executor.run_until_parked(); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(zed_id), &[client_a.user_id().unwrap()], ); assert_participants_eq(channels.channel_participants(rust_id), &[]); }) }); active_call_a .update(cx_a, |active_call, cx| { active_call.join_channel(rust_id, cx) }) .await .unwrap(); executor.run_until_parked(); cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq(channels.channel_participants(zed_id), &[]); assert_participants_eq( channels.channel_participants(rust_id), &[client_a.user_id().unwrap()], ); }) }); } #[gpui::test] async fn test_permissions_update_while_invited( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server .make_channel("rust", None, (&client_a, cx_a), &mut []) .await; client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.invite_member( rust_id, client_b.user_id().unwrap(), proto::ChannelRole::Member, cx, ) }) .await .unwrap(); executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), cx_b, &[ExpectedChannel { depth: 0, id: rust_id, name: "rust".into(), }], ); assert_channels(client_b.channel_store(), cx_b, &[]); // Update B's invite before they've accepted it client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_member_role( rust_id, client_b.user_id().unwrap(), proto::ChannelRole::Admin, cx, ) }) .await .unwrap(); executor.run_until_parked(); assert_channel_invitations( client_b.channel_store(), cx_b, &[ExpectedChannel { depth: 0, id: rust_id, name: "rust".into(), }], ); assert_channels(client_b.channel_store(), cx_b, &[]); } #[gpui::test] async fn test_channel_rename( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let rust_id = server .make_channel("rust", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; // Rename the channel client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.rename(rust_id, "#rust-archive", cx) }) .await .unwrap(); executor.run_until_parked(); // Client A sees the channel with its new name. assert_channels( client_a.channel_store(), cx_a, &[ExpectedChannel { depth: 0, id: rust_id, name: "rust-archive".into(), }], ); // Client B sees the channel with its new name. assert_channels( client_b.channel_store(), cx_b, &[ExpectedChannel { depth: 0, id: rust_id, name: "rust-archive".into(), }], ); } #[gpui::test] async fn test_call_from_channel( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; server .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let channel_id = server .make_channel( "x", None, (&client_a, cx_a), &mut [(&client_b, cx_b), (&client_c, cx_c)], ) .await; let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); active_call_a .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); // Client A calls client B while in the channel. active_call_a .update(cx_a, |call, cx| { call.invite(client_b.user_id().unwrap(), None, cx) }) .await .unwrap(); // Client B accepts the call. executor.run_until_parked(); active_call_b .update(cx_b, |call, cx| call.accept_incoming(cx)) .await .unwrap(); // Client B sees that they are now in the channel executor.run_until_parked(); cx_b.read(|cx| { active_call_b.read_with(cx, |call, cx| { assert_eq!(call.channel_id(cx), Some(channel_id)); }) }); cx_b.read(|cx| { client_b.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(channel_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); // Clients A and C also see that client B is in the channel. cx_a.read(|cx| { client_a.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(channel_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); cx_c.read(|cx| { client_c.channel_store().read_with(cx, |channels, _| { assert_participants_eq( channels.channel_participants(channel_id), &[client_a.user_id().unwrap(), client_b.user_id().unwrap()], ); }) }); } #[gpui::test] async fn test_lost_channel_creation( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; server .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .await; let channel_id = server .make_channel("x", None, (&client_a, cx_a), &mut []) .await; // Invite a member client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.invite_member( channel_id, client_b.user_id().unwrap(), proto::ChannelRole::Member, cx, ) }) .await .unwrap(); executor.run_until_parked(); // Sanity check, B has the invitation assert_channel_invitations( client_b.channel_store(), cx_b, &[ExpectedChannel { depth: 0, id: channel_id, name: "x".into(), }], ); // A creates a subchannel while the invite is still pending. let subchannel_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("subchannel", Some(channel_id), cx) }) .await .unwrap(); executor.run_until_parked(); // Make sure A sees their new channel assert_channels( client_a.channel_store(), cx_a, &[ ExpectedChannel { depth: 0, id: channel_id, name: "x".into(), }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".into(), }, ], ); // Client B accepts the invite client_b .channel_store() .update(cx_b, |channel_store, cx| { channel_store.respond_to_channel_invite(channel_id, true, cx) }) .await .unwrap(); executor.run_until_parked(); // Client B should now see the channel assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { depth: 0, id: channel_id, name: "x".into(), }, ExpectedChannel { depth: 1, id: subchannel_id, name: "subchannel".into(), }, ], ); } #[gpui::test] async fn test_channel_link_notifications( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, cx_c: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let client_c = server.create_client(cx_c, "user_c").await; let user_b = client_b.user_id().unwrap(); let user_c = client_c.user_id().unwrap(); let channels = server .make_channel_tree(&[("zed", None)], (&client_a, cx_a)) .await; let zed_channel = channels[0]; try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { [ channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Member, cx), channel_store.invite_member(zed_channel, user_c, proto::ChannelRole::Guest, cx), ] })) .await .unwrap(); executor.run_until_parked(); client_b .channel_store() .update(cx_b, |channel_store, cx| { channel_store.respond_to_channel_invite(zed_channel, true, cx) }) .await .unwrap(); client_c .channel_store() .update(cx_c, |channel_store, cx| { channel_store.respond_to_channel_invite(zed_channel, true, cx) }) .await .unwrap(); executor.run_until_parked(); // we have an admin (a), member (b) and guest (c) all part of the zed channel. // create a new private channel, make it public, and move it under the previous one, and verify it shows for b and not c let active_channel = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("active", Some(zed_channel), cx) }) .await .unwrap(); executor.run_until_parked(); // the new channel shows for b and not c assert_channels_list_shape( client_a.channel_store(), cx_a, &[(zed_channel, 0), (active_channel, 1)], ); assert_channels_list_shape( client_b.channel_store(), cx_b, &[(zed_channel, 0), (active_channel, 1)], ); assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]); let vim_channel = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("vim", Some(zed_channel), cx) }) .await .unwrap(); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx) }) .await .unwrap(); // the new channel shows for b and c assert_channels_list_shape( client_a.channel_store(), cx_a, &[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)], ); assert_channels_list_shape( client_b.channel_store(), cx_b, &[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)], ); assert_channels_list_shape( client_c.channel_store(), cx_c, &[(zed_channel, 0), (vim_channel, 1)], ); let helix_channel = client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.create_channel("helix", Some(zed_channel), cx) }) .await .unwrap(); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.move_channel(helix_channel, vim_channel, cx) }) .await .unwrap(); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_channel_visibility( helix_channel, proto::ChannelVisibility::Public, cx, ) }) .await .unwrap(); cx_a.run_until_parked(); // the new channel shows for b and c assert_channels_list_shape( client_b.channel_store(), cx_b, &[ (zed_channel, 0), (active_channel, 1), (vim_channel, 1), (helix_channel, 2), ], ); assert_channels_list_shape( client_c.channel_store(), cx_c, &[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)], ); } #[gpui::test] async fn test_channel_membership_notifications( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_c").await; let user_b = client_b.user_id().unwrap(); let channels = server .make_channel_tree( &[("zed", None), ("vim", Some("zed")), ("opensource", None)], (&client_a, cx_a), ) .await; let zed_channel = channels[0]; let vim_channel = channels[1]; let opensource_channel = channels[2]; try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| { [ channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx), channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx), channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx), channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx), ] })) .await .unwrap(); executor.run_until_parked(); client_b .channel_store() .update(cx_b, |channel_store, cx| { channel_store.respond_to_channel_invite(zed_channel, true, cx) }) .await .unwrap(); executor.run_until_parked(); // we have an admin (a), and a guest (b) with access to all of zed, and membership in vim. assert_channels( client_b.channel_store(), cx_b, &[ ExpectedChannel { depth: 0, id: zed_channel, name: "zed".into(), }, ExpectedChannel { depth: 1, id: vim_channel, name: "vim".into(), }, ], ); client_b.channel_store().update(cx_b, |channel_store, _| { channel_store.is_channel_admin(zed_channel) }); client_b .channel_store() .update(cx_b, |channel_store, cx| { channel_store.respond_to_channel_invite(opensource_channel, true, cx) }) .await .unwrap(); cx_a.run_until_parked(); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx) }) .await .unwrap(); cx_a.run_until_parked(); client_b.channel_store().update(cx_b, |channel_store, _| { channel_store.is_channel_admin(opensource_channel) }); } #[gpui::test] async fn test_guest_access( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let channels = server .make_channel_tree( &[("channel-a", None), ("channel-b", Some("channel-a"))], (&client_a, cx_a), ) .await; let channel_a = channels[0]; let channel_b = channels[1]; let active_call_b = cx_b.read(ActiveCall::global); // Non-members should not be allowed to join assert!(active_call_b .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .is_err()); // Make channels A and B public client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Public, cx) }) .await .unwrap(); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.set_channel_visibility(channel_b, proto::ChannelVisibility::Public, cx) }) .await .unwrap(); // Client B joins channel A as a guest active_call_b .update(cx_b, |call, cx| call.join_channel(channel_a, cx)) .await .unwrap(); executor.run_until_parked(); assert_channels_list_shape( client_a.channel_store(), cx_a, &[(channel_a, 0), (channel_b, 1)], ); assert_channels_list_shape( client_b.channel_store(), cx_b, &[(channel_a, 0), (channel_b, 1)], ); client_a.channel_store().update(cx_a, |channel_store, _| { let participants = channel_store.channel_participants(channel_a); assert_eq!(participants.len(), 1); assert_eq!(participants[0].id, client_b.user_id().unwrap()); }); } #[gpui::test] async fn test_invite_access( executor: BackgroundExecutor, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; let channels = server .make_channel_tree( &[("channel-a", None), ("channel-b", Some("channel-a"))], (&client_a, cx_a), ) .await; let channel_a_id = channels[0]; let channel_b_id = channels[0]; let active_call_b = cx_b.read(ActiveCall::global); // should not be allowed to join assert!(active_call_b .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .is_err()); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.invite_member( channel_a_id, client_b.user_id().unwrap(), ChannelRole::Member, cx, ) }) .await .unwrap(); active_call_b .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx)) .await .unwrap(); executor.run_until_parked(); client_b.channel_store().update(cx_b, |channel_store, _| { assert!(channel_store.channel_for_id(channel_b_id).is_some()); assert!(channel_store.channel_for_id(channel_a_id).is_some()); }); client_a.channel_store().update(cx_a, |channel_store, _| { let participants = channel_store.channel_participants(channel_b_id); assert_eq!(participants.len(), 1); assert_eq!(participants[0].id, client_b.user_id().unwrap()); }) } #[gpui::test] async fn test_leave_channel(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let (_server, _client_a, client_b, channel_id) = TestServer::start2(cx_a, cx_b).await; client_b .channel_store() .update(cx_b, |channel_store, cx| { channel_store.remove_member(channel_id, client_b.user_id().unwrap(), cx) }) .await .unwrap(); cx_a.run_until_parked(); assert_eq!( client_b .channel_store() .read_with(cx_b, |store, _| store.channels().count()), 0 ); } #[gpui::test] async fn test_channel_moving( executor: BackgroundExecutor, cx_a: &mut TestAppContext, _cx_b: &mut TestAppContext, _cx_c: &mut TestAppContext, ) { let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let channels = server .make_channel_tree( &[ ("channel-a", None), ("channel-b", Some("channel-a")), ("channel-c", Some("channel-b")), ("channel-d", Some("channel-c")), ], (&client_a, cx_a), ) .await; let channel_a_id = channels[0]; let channel_b_id = channels[1]; let channel_c_id = channels[2]; let channel_d_id = channels[3]; // Current shape: // a - b - c - d assert_channels_list_shape( client_a.channel_store(), cx_a, &[ (channel_a_id, 0), (channel_b_id, 1), (channel_c_id, 2), (channel_d_id, 3), ], ); client_a .channel_store() .update(cx_a, |channel_store, cx| { channel_store.move_channel(channel_d_id, channel_b_id, cx) }) .await .unwrap(); // Current shape: // /- d // a - b -- c assert_channels_list_shape( client_a.channel_store(), cx_a, &[ (channel_a_id, 0), (channel_b_id, 1), (channel_c_id, 2), (channel_d_id, 2), ], ); } #[derive(Debug, PartialEq)] struct ExpectedChannel { depth: usize, id: ChannelId, name: SharedString, } #[track_caller] fn assert_channel_invitations( channel_store: &Model, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { let actual = cx.read(|cx| { channel_store.read_with(cx, |store, _| { store .channel_invitations() .iter() .map(|channel| ExpectedChannel { depth: 0, name: channel.name.clone(), id: channel.id, }) .collect::>() }) }); assert_eq!(actual, expected_channels); } #[track_caller] fn assert_channels( channel_store: &Model, cx: &TestAppContext, expected_channels: &[ExpectedChannel], ) { let actual = cx.read(|cx| { channel_store.read_with(cx, |store, _| { store .ordered_channels() .map(|(depth, channel)| ExpectedChannel { depth, name: channel.name.clone().into(), id: channel.id, }) .collect::>() }) }); pretty_assertions::assert_eq!(actual, expected_channels); } #[track_caller] fn assert_channels_list_shape( channel_store: &Model, cx: &TestAppContext, expected_channels: &[(u64, usize)], ) { let actual = cx.read(|cx| { channel_store.read_with(cx, |store, _| { store .ordered_channels() .map(|(depth, channel)| (channel.id, depth)) .collect::>() }) }); pretty_assertions::assert_eq!(actual, expected_channels); }