You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/synctv-api/tests/client_resource_availabilit...

1034 lines
32 KiB
Rust

#![allow(clippy::unwrap_used)]
mod support;
use synctv_api::ApiRuntimeSettings as Config;
use std::sync::Arc;
use chrono::Utc;
use synctv_api::ClientApiImpl;
use synctv_core::{
cache::{KeyBuilder, UsernameCache},
models::{
room_settings::AllowGuestJoin, Media, MediaId, Playlist, PlaylistId, RoomId, SignupMethod,
User, UserId, UserRole, UserStatus,
},
repository::{MediaRepository, PlaylistRepository, SettingsRepository, UserRepository},
service::{
BruteForceProtection, InMemoryTokenBlacklistStore, JwtService, RoomService,
RoomServiceOptions, RuntimeSettingsStore, SettingsService, UserService,
},
};
use synctv_core_testing::{create_test_pool, create_test_pool_with_db_and_label, TestContainer};
use synctv_realtime::sync::{ConnectionLimits, ConnectionManager};
fn public_id_codec() -> synctv_api::PublicIdCodec {
synctv_api::PublicIdCodec::plain()
}
fn make_user(username: &str) -> User {
let now = Utc::now();
User {
id: UserId::new(),
username: username.to_string(),
role: UserRole::User,
avatar_file_reference_id: None,
status: UserStatus::Active,
is_banned: false,
banned_at: None,
banned_by: None,
banned_reason: None,
signup_method: SignupMethod::Email,
created_at: now,
updated_at: now,
version: 0,
deleted_at: None,
}
}
fn make_user_service(pool: &sqlx::PgPool) -> UserService {
let jwt_service = JwtService::new("Test_Secret_Key_For_JWT_Tokens_32Bytes!!").unwrap();
let username_cache = UsernameCache::local_only("test:username:".to_string(), 100, 60);
let token_blacklist = Arc::new(InMemoryTokenBlacklistStore::new(1000, 3600, 86400));
UserService::new_for_tests(
pool,
jwt_service,
username_cache,
token_blacklist,
KeyBuilder::new("test"),
BruteForceProtection::in_memory("test:user".to_string()),
)
}
fn make_playlist(room_id: &RoomId, creator_id: &UserId, name: &str, position: i32) -> Playlist {
let now = Utc::now();
Playlist {
id: PlaylistId::new(),
room_id: *room_id,
creator_id: Some(*creator_id),
browse_access_mode: synctv_core::models::PlaylistBrowseAccessMode::Default,
name: name.to_string(),
description: String::new(),
cover_file_reference_id: None,
parent_id: None,
position: f64::from(position),
source_provider: None,
source_config: None,
provider_instance_name: None,
created_at: now,
updated_at: now,
version: 0,
}
}
fn make_dynamic_playlist(
room_id: &RoomId,
creator_id: &UserId,
name: &str,
position: i32,
) -> Playlist {
let mut playlist = make_playlist(room_id, creator_id, name, position);
playlist.source_provider = Some(synctv_core::models::SourceProvider::Alist);
playlist.source_config = Some(synctv_core_testing::alist_directory_playlist_source_config(
"alist-test",
"/library",
));
playlist
}
fn make_media(room_id: &RoomId, creator_id: &UserId, name: &str, position: i32) -> Media {
let now = Utc::now();
Media {
id: MediaId::new(),
playlist_id: None,
room_id: *room_id,
creator_id: Some(*creator_id),
name: name.to_string(),
description: String::new(),
position: f64::from(position),
source_provider: synctv_core::models::SourceProvider::DirectUrl,
source_config: synctv_core_testing::direct_url_media_source_config(format!(
"https://example.com/{name}.mp4"
)),
provider_instance_name: None,
cover_file_reference_id: None,
thumbnail_file_reference_id: None,
added_at: now,
updated_at: now,
version: 0,
}
}
struct ClientApiFixture {
postgres: TestContainer,
pool: sqlx::PgPool,
client_api: ClientApiImpl,
user_repo: UserRepository,
playlist_repo: PlaylistRepository,
media_repo: MediaRepository,
owner: User,
creator: User,
room: synctv_core::models::Room,
}
impl ClientApiFixture {
async fn cleanup(self) {
let Self {
postgres,
pool,
client_api,
user_repo,
playlist_repo,
media_repo,
owner,
creator,
room,
} = self;
drop(client_api);
drop(user_repo);
drop(playlist_repo);
drop(media_repo);
drop(owner);
drop(creator);
drop(room);
pool.close().await;
postgres.cleanup().await;
}
}
async fn create_client_api_fixture() -> ClientApiFixture {
let (postgres, pool) = create_test_pool().await;
let user_repo = UserRepository::new(pool.clone());
let playlist_repo = PlaylistRepository::new(pool.clone());
let media_repo = MediaRepository::new(pool.clone());
let user_service = Arc::new(make_user_service(&pool));
let settings_service = Arc::new(SettingsService::new(
SettingsRepository::new(pool.clone()),
pool.clone(),
));
settings_service.initialize().await.unwrap();
let runtime_settings_store = Arc::new(RuntimeSettingsStore::new(settings_service));
let room_service = RoomService::new_with_options(
pool.clone(),
(*user_service).clone(),
RoomServiceOptions {
runtime_settings_store: Some(runtime_settings_store.clone()),
..RoomServiceOptions::test_defaults_with_settings(pool.clone())
},
)
.expect("room service should build");
let room_service = Arc::new(room_service);
let owner = user_repo
.create(&make_user("availability_owner"))
.await
.unwrap();
let creator = user_repo
.create(&make_user("availability_creator"))
.await
.unwrap();
let room = room_service
.create_room(
"Availability Room".to_string(),
String::new(),
owner.id,
None,
None,
)
.await
.unwrap();
let connection_service = Arc::new(ConnectionManager::new(ConnectionLimits::default()));
let mut runtime = support::client_api_runtime();
runtime.presence_service = connection_service.presence_service();
let client_api = ClientApiImpl::new_with_runtime(
synctv_api::ClientApiOptions {
read_pool: None,
user_service,
room_service,
connection_service,
runtime_settings: Arc::new(Config::default()),
publish_key_service: None,
jwt_service: JwtService::new("Test_Secret_Key_For_JWT_Tokens_32Bytes!!").unwrap(),
live_streaming_infrastructure: None,
runtime_settings_store: Some(runtime_settings_store),
public_id_codec: Arc::new(synctv_api::PublicIdCodec::plain()),
chat_service: None,
provider_stores: Arc::new(synctv_core::provider::ProviderStoreRegistry::local_only(
"test:provider:",
)),
email_api: None,
passkey_service: None,
},
runtime,
);
ClientApiFixture {
postgres,
pool,
client_api,
user_repo,
playlist_repo,
media_repo,
owner,
creator,
room,
}
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn single_room_discovery_uses_primary_while_feed_uses_read_pool() {
let (primary_postgres, primary_pool) =
create_test_pool_with_db_and_label("synctv_test", "discovery-primary").await;
let (read_postgres, read_pool) =
create_test_pool_with_db_and_label("synctv_test", "discovery-read").await;
let user_repo = UserRepository::new(primary_pool.clone());
let user_service = Arc::new(make_user_service(&primary_pool));
let settings_service = Arc::new(SettingsService::new(
SettingsRepository::new(primary_pool.clone()),
primary_pool.clone(),
));
settings_service.initialize().await.unwrap();
let runtime_settings_store = Arc::new(RuntimeSettingsStore::new(settings_service));
let room_service = Arc::new(
RoomService::new_with_options(
primary_pool.clone(),
(*user_service).clone(),
RoomServiceOptions {
read_pool: Some(read_pool.clone()),
runtime_settings_store: Some(runtime_settings_store.clone()),
..RoomServiceOptions::test_defaults_with_settings(primary_pool.clone())
},
)
.expect("room service should build"),
);
let owner = user_repo
.create(&make_user("primary_discovery_owner"))
.await
.unwrap();
let room = room_service
.create_room(
"Primary Discovery Room".to_string(),
String::new(),
owner.id,
None,
None,
)
.await
.unwrap();
let connection_service = Arc::new(ConnectionManager::new(ConnectionLimits::default()));
let mut runtime = support::client_api_runtime();
runtime.presence_service = connection_service.presence_service();
let client_api = ClientApiImpl::new_with_runtime(
synctv_api::ClientApiOptions {
read_pool: Some(read_pool.clone()),
user_service,
room_service,
connection_service,
runtime_settings: Arc::new(Config::default()),
publish_key_service: None,
jwt_service: JwtService::new("Test_Secret_Key_For_JWT_Tokens_32Bytes!!").unwrap(),
live_streaming_infrastructure: None,
runtime_settings_store: Some(runtime_settings_store),
public_id_codec: Arc::new(synctv_api::PublicIdCodec::plain()),
chat_service: None,
provider_stores: Arc::new(synctv_core::provider::ProviderStoreRegistry::local_only(
"test:provider:",
)),
email_api: None,
passkey_service: None,
},
runtime,
);
let room_public_id = public_id_codec().encode_room_id(room.id).unwrap();
let feed = client_api
.discover_public_rooms(synctv_proto::client::DiscoverRoomsRequest {
page: 1,
page_size: 20,
search: String::new(),
category_id: String::new(),
label_ids: Vec::new(),
})
.await
.unwrap();
assert!(feed.featured_rooms.is_empty());
assert!(feed.rooms.is_empty());
let public_item = client_api
.get_public_room_discovery(synctv_proto::client::GetRoomDiscoveryRequest {
room_id: room_public_id.clone(),
})
.await
.unwrap();
assert_eq!(public_item.room.as_ref().unwrap().name, room.name);
assert_eq!(public_item.room.as_ref().unwrap().member_count, 1);
let user_item = client_api
.get_room_discovery(
&owner.id,
synctv_proto::client::GetRoomDiscoveryRequest {
room_id: room_public_id,
},
)
.await
.unwrap();
assert!(user_item.joined);
assert_eq!(
user_item.access,
synctv_proto::client::RoomDiscoveryAccess::Enter as i32
);
drop(client_api);
drop(user_repo);
primary_pool.close().await;
read_pool.close().await;
primary_postgres.cleanup().await;
read_postgres.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn public_room_discovery_exposes_direct_guest_entry_and_token_flow() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
room,
owner,
..
} = &fixture;
let mut settings = client_api
.room_service
.get_room_settings(&room.id)
.await
.unwrap();
settings.allow_guest_join = AllowGuestJoin(true);
client_api
.room_service
.set_settings(room.id, owner.id, settings)
.await
.unwrap();
let response = client_api
.discover_public_rooms(synctv_proto::client::DiscoverRoomsRequest {
page: 1,
page_size: 20,
search: room.name.clone(),
category_id: String::new(),
label_ids: Vec::new(),
})
.await
.unwrap();
let room_public_id = public_id_codec().encode_room_id(room.id).unwrap();
let item = response
.rooms
.iter()
.find(|item| {
item.room
.as_ref()
.is_some_and(|listed| listed.id == room_public_id)
})
.expect("guest-enabled room should be discoverable");
assert!(item.can_join);
assert!(!item.joined);
assert!(!item.favorited);
assert_eq!(
item.access,
synctv_proto::client::RoomDiscoveryAccess::Guest as i32
);
let token = client_api
.create_guest_token_with_control(
synctv_proto::client::CreateGuestTokenRequest {
room_id: room_public_id,
},
None,
)
.await
.unwrap();
assert!(!token.token.is_empty());
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn old_guest_token_is_rejected_after_room_creator_becomes_inactive() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
user_repo,
room,
owner,
..
} = &fixture;
let mut settings = client_api
.room_service
.get_room_settings(&room.id)
.await
.unwrap();
settings.allow_guest_join = AllowGuestJoin(true);
client_api
.room_service
.set_settings(room.id, owner.id, settings)
.await
.unwrap();
let room_public_id = public_id_codec().encode_room_id(room.id).unwrap();
let token = client_api
.create_guest_token_with_control(
synctv_proto::client::CreateGuestTokenRequest {
room_id: room_public_id.clone(),
},
None,
)
.await
.unwrap();
client_api
.validate_guest_room_access(&token.token, &room_public_id)
.await
.unwrap();
user_repo.ban(&owner.id, None, None).await.unwrap();
let old_token_error = client_api
.validate_guest_room_access(&token.token, &room_public_id)
.await
.unwrap_err();
assert!(
matches!(old_token_error, synctv_api::ApiError::Authorization(_)),
"old guest tokens must stop authorizing access when the room creator is inactive"
);
let new_token_error = client_api
.create_guest_token_with_control(
synctv_proto::client::CreateGuestTokenRequest {
room_id: room_public_id,
},
None,
)
.await
.unwrap_err();
assert!(
matches!(new_token_error, synctv_api::ApiError::Authorization(_)),
"inactive room creators must prevent new guest token issuance"
);
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn list_playlist_items_root_includes_unavailable_resources_and_marks_availability() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
user_repo,
playlist_repo,
media_repo,
owner,
creator,
room,
..
} = &fixture;
let available_playlist = playlist_repo
.create(&make_playlist(&room.id, &owner.id, "available-folder", 1))
.await
.unwrap();
let unavailable_playlist = playlist_repo
.create(&make_dynamic_playlist(
&room.id,
&creator.id,
"unavailable-folder",
2,
))
.await
.unwrap();
let available_media = media_repo
.create(&make_media(&room.id, &owner.id, "available-media", 1))
.await
.unwrap();
let unavailable_media = media_repo
.create(&make_media(&room.id, &creator.id, "unavailable-media", 2))
.await
.unwrap();
user_repo.ban(&creator.id, None, None).await.unwrap();
let codec = public_id_codec();
let room_public_id = codec.encode_room_id(room.id).unwrap();
let available_playlist_id = codec.encode_playlist_id(available_playlist.id).unwrap();
let unavailable_playlist_id = codec.encode_playlist_id(unavailable_playlist.id).unwrap();
let available_media_id = codec.encode_media_id(available_media.id).unwrap();
let unavailable_media_id = codec.encode_media_id(unavailable_media.id).unwrap();
let response = client_api
.list_playlist_items(
&owner.id,
&room_public_id,
synctv_proto::client::ListPlaylistItemsRequest {
playlist_id: String::new(),
target: None,
pagination: Some(
synctv_proto::client::list_playlist_items_request::Pagination::Page(
synctv_proto::client::PagePagination { page: 1 },
),
),
page_size: 20,
search: String::new(),
source_provider: synctv_proto::source_config::SourceProvider::Unspecified as i32,
provider_instance_name: String::new(),
sort_by: synctv_proto::client::MediaListSortBy::Position as i32,
sort_direction: synctv_proto::client::SortDirection::Asc as i32,
availability: synctv_proto::client::ResourceAvailabilityFilter::All as i32,
refresh: false,
preview_source_config: None,
},
)
.await
.unwrap();
assert_eq!(response.total, Some(4));
assert_eq!(response.playlist_count, 2);
assert_eq!(response.file_count, 2);
assert_eq!(response.playlists.len(), 2);
assert_eq!(response.media.len(), 2);
let available_folder = response
.playlists
.iter()
.find(|item| item.id == available_playlist_id)
.unwrap();
assert_eq!(
available_folder.availability,
synctv_proto::client::ResourceAvailability::Available as i32
);
let unavailable_folder = response
.playlists
.iter()
.find(|item| item.id == unavailable_playlist_id)
.unwrap();
assert_eq!(
unavailable_folder.availability,
synctv_proto::client::ResourceAvailability::CreatorInactive as i32
);
let available_file = response
.media
.iter()
.find(|item| item.id == available_media_id)
.unwrap();
assert_eq!(
available_file.availability,
synctv_proto::client::ResourceAvailability::Available as i32
);
let unavailable_file = response
.media
.iter()
.find(|item| item.id == unavailable_media_id)
.unwrap();
assert_eq!(
unavailable_file.availability,
synctv_proto::client::ResourceAvailability::CreatorInactive as i32
);
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn list_playlist_items_root_availability_filter_updates_counts_and_pagination() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
user_repo,
playlist_repo,
media_repo,
owner,
creator,
room,
..
} = &fixture;
playlist_repo
.create(&make_playlist(&room.id, &owner.id, "available-folder", 1))
.await
.unwrap();
let unavailable_playlist = playlist_repo
.create(&make_dynamic_playlist(
&room.id,
&creator.id,
"unavailable-folder",
0,
))
.await
.unwrap();
media_repo
.create(&make_media(&room.id, &owner.id, "available-media", 1))
.await
.unwrap();
let unavailable_media = media_repo
.create(&make_media(&room.id, &creator.id, "unavailable-media", 0))
.await
.unwrap();
user_repo.ban(&creator.id, None, None).await.unwrap();
let codec = public_id_codec();
let room_public_id = codec.encode_room_id(room.id).unwrap();
let unavailable_playlist_id = codec.encode_playlist_id(unavailable_playlist.id).unwrap();
let unavailable_media_id = codec.encode_media_id(unavailable_media.id).unwrap();
let page_one = client_api
.list_playlist_items(
&owner.id,
&room_public_id,
synctv_proto::client::ListPlaylistItemsRequest {
playlist_id: String::new(),
target: None,
pagination: Some(
synctv_proto::client::list_playlist_items_request::Pagination::Page(
synctv_proto::client::PagePagination { page: 1 },
),
),
page_size: 1,
search: String::new(),
source_provider: synctv_proto::source_config::SourceProvider::Unspecified as i32,
provider_instance_name: String::new(),
sort_by: synctv_proto::client::MediaListSortBy::Position as i32,
sort_direction: synctv_proto::client::SortDirection::Asc as i32,
availability: synctv_proto::client::ResourceAvailabilityFilter::Unavailable as i32,
refresh: false,
preview_source_config: None,
},
)
.await
.unwrap();
assert_eq!(page_one.total, Some(2));
assert_eq!(page_one.playlist_count, 1);
assert_eq!(page_one.file_count, 1);
assert_eq!(page_one.playlists.len(), 1);
assert!(page_one.media.is_empty());
assert_eq!(page_one.playlists[0].id, unavailable_playlist_id);
let page_two = client_api
.list_playlist_items(
&owner.id,
&room_public_id,
synctv_proto::client::ListPlaylistItemsRequest {
playlist_id: String::new(),
target: None,
pagination: Some(
synctv_proto::client::list_playlist_items_request::Pagination::Page(
synctv_proto::client::PagePagination { page: 2 },
),
),
page_size: 1,
search: String::new(),
source_provider: synctv_proto::source_config::SourceProvider::Unspecified as i32,
provider_instance_name: String::new(),
sort_by: synctv_proto::client::MediaListSortBy::Position as i32,
sort_direction: synctv_proto::client::SortDirection::Asc as i32,
availability: synctv_proto::client::ResourceAvailabilityFilter::Unavailable as i32,
refresh: false,
preview_source_config: None,
},
)
.await
.unwrap();
assert_eq!(page_two.total, Some(2));
assert_eq!(page_two.playlist_count, 1);
assert_eq!(page_two.file_count, 1);
assert!(page_two.playlists.is_empty());
assert_eq!(page_two.media.len(), 1);
assert_eq!(page_two.media[0].id, unavailable_media_id);
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn list_playlist_items_root_returns_stable_version_until_contents_change() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
media_repo,
owner,
room,
..
} = &fixture;
media_repo
.create(&make_media(&room.id, &owner.id, "media-one", 1))
.await
.unwrap();
let request = synctv_proto::client::ListPlaylistItemsRequest {
playlist_id: String::new(),
target: None,
pagination: Some(
synctv_proto::client::list_playlist_items_request::Pagination::Page(
synctv_proto::client::PagePagination { page: 1 },
),
),
page_size: 20,
search: String::new(),
source_provider: synctv_proto::source_config::SourceProvider::Unspecified as i32,
provider_instance_name: String::new(),
sort_by: synctv_proto::client::MediaListSortBy::Position as i32,
sort_direction: synctv_proto::client::SortDirection::Asc as i32,
availability: synctv_proto::client::ResourceAvailabilityFilter::All as i32,
refresh: false,
preview_source_config: None,
};
let room_public_id = public_id_codec().encode_room_id(room.id).unwrap();
let first = client_api
.list_playlist_items(&owner.id, &room_public_id, request.clone())
.await
.unwrap();
let second = client_api
.list_playlist_items(&owner.id, &room_public_id, request.clone())
.await
.unwrap();
assert!(!first.version.is_empty());
assert_eq!(first.version, second.version);
media_repo
.create(&make_media(&room.id, &owner.id, "media-two", 2))
.await
.unwrap();
let third = client_api
.list_playlist_items(&owner.id, &room_public_id, request)
.await
.unwrap();
assert_ne!(first.version, third.version);
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn list_playlists_availability_filter_updates_total_and_response_items() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
user_repo,
playlist_repo,
owner,
creator,
room,
..
} = &fixture;
playlist_repo
.create(&make_playlist(&room.id, &owner.id, "available-folder", 1))
.await
.unwrap();
let unavailable_playlist = playlist_repo
.create(&make_dynamic_playlist(
&room.id,
&creator.id,
"unavailable-folder",
0,
))
.await
.unwrap();
user_repo.ban(&creator.id, None, None).await.unwrap();
let codec = public_id_codec();
let room_public_id = codec.encode_room_id(room.id).unwrap();
let unavailable_playlist_id = codec.encode_playlist_id(unavailable_playlist.id).unwrap();
let response = client_api
.list_playlists(
&owner.id,
&room_public_id,
synctv_proto::client::ListPlaylistsRequest {
parent_id: String::new(),
page: 1,
page_size: 20,
search: String::new(),
source_provider: synctv_proto::source_config::SourceProvider::Unspecified as i32,
provider_instance_name: String::new(),
dynamic_only: None,
sort_by: synctv_proto::client::PlaylistListSortBy::Position as i32,
sort_direction: synctv_proto::client::SortDirection::Asc as i32,
availability: synctv_proto::client::ResourceAvailabilityFilter::Unavailable as i32,
},
)
.await
.unwrap();
assert_eq!(response.total, 1);
assert_eq!(response.playlists.len(), 1);
assert_eq!(response.playlists[0].id, unavailable_playlist_id);
assert_eq!(
response.playlists[0].availability,
synctv_proto::client::ResourceAvailability::CreatorInactive as i32
);
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn public_room_discovery_marks_room_unavailable_when_creator_is_banned() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
user_repo,
room,
owner,
..
} = &fixture;
user_repo.ban(&owner.id, None, None).await.unwrap();
let connection_id = format!("conn_{}", uuid::Uuid::new_v4().simple());
client_api
.connection_service
.register(connection_id.clone(), owner.id)
.await
.unwrap();
client_api
.connection_service
.join_room(&connection_id, room.id)
.await
.unwrap();
let list_response = client_api
.discover_public_rooms(synctv_proto::client::DiscoverRoomsRequest {
page: 1,
page_size: 20,
search: String::new(),
category_id: String::new(),
label_ids: Vec::new(),
})
.await
.unwrap();
let room_public_id = public_id_codec().encode_room_id(room.id).unwrap();
let listed_room = list_response
.featured_rooms
.iter()
.chain(list_response.rooms.iter())
.find(|item| {
item.room
.as_ref()
.is_some_and(|room| room.id == room_public_id)
})
.expect("public list should still surface the room");
let listed_room = listed_room.room.as_ref().unwrap();
assert_eq!(
listed_room.availability,
synctv_proto::client::ResourceAvailability::CreatorInactive as i32
);
let discovery_item = client_api
.get_public_room_discovery(synctv_proto::client::GetRoomDiscoveryRequest {
room_id: room_public_id.clone(),
})
.await
.unwrap();
let discovered_room = discovery_item.room.as_ref().unwrap();
assert_eq!(
discovered_room.availability,
synctv_proto::client::ResourceAvailability::CreatorInactive as i32
);
assert_eq!(discovered_room.name, room.name);
let authenticated = client_api
.discover_rooms(
&owner.id,
synctv_proto::client::DiscoverRoomsRequest {
page: 1,
page_size: 20,
search: room.name.clone(),
category_id: String::new(),
label_ids: Vec::new(),
},
)
.await
.unwrap();
let joined_item = authenticated
.rooms
.iter()
.find(|item| {
item.room
.as_ref()
.is_some_and(|listed| listed.id == room_public_id)
})
.expect("joined room should remain visible to its member");
assert!(joined_item.joined);
assert!(!joined_item.can_join);
assert_eq!(
joined_item.access,
synctv_proto::client::RoomDiscoveryAccess::Unavailable as i32
);
client_api
.connection_service
.unregister(&connection_id)
.await;
fixture.cleanup().await;
}
#[tokio::test]
#[ignore = "Requires Docker"]
async fn room_discovery_prioritizes_online_rooms_outside_newest_page() {
let fixture = Box::pin(create_client_api_fixture()).await;
let ClientApiFixture {
client_api,
room,
owner,
..
} = &fixture;
let connection_id = format!("conn_{}", uuid::Uuid::new_v4().simple());
client_api
.connection_service
.register(connection_id.clone(), owner.id)
.await
.unwrap();
client_api
.connection_service
.join_room(&connection_id, room.id)
.await
.unwrap();
for index in 0..5 {
client_api
.room_service
.create_room(
format!("newer-empty-room-{index}"),
String::new(),
owner.id,
None,
None,
)
.await
.unwrap();
}
let response = client_api
.discover_public_rooms(synctv_proto::client::DiscoverRoomsRequest {
page: 1,
page_size: 1,
search: String::new(),
category_id: String::new(),
label_ids: Vec::new(),
})
.await
.unwrap();
let top_room = response
.featured_rooms
.first()
.expect("room discovery should return one featured room");
let top_room = top_room.room.as_ref().unwrap();
assert_eq!(
top_room.id,
public_id_codec().encode_room_id(room.id).unwrap()
);
assert_eq!(
top_room
.presence
.as_ref()
.map(|value| value.online_member_count),
Some(1)
);
assert_eq!(
top_room
.presence
.as_ref()
.map(|value| value.online_guest_count),
Some(0)
);
let first_popular_room = response
.rooms
.first()
.expect("room discovery should return one non-featured room");
let first_popular_room = first_popular_room.room.as_ref().unwrap();
assert_ne!(first_popular_room.id, top_room.id);
client_api
.connection_service
.unregister(&connection_id)
.await;
fixture.cleanup().await;
}