|
|
|
|
#![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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix(room): enforce member-owned resource lifecycle (#425)
## Summary
- preserve room membership and owned resources across global bans while
blocking browsing and playback until unban
- clean up member-owned media and dynamic playlists on leave, kick, and
account deletion while preserving ordinary shared playlists
- enforce lifecycle checks for playback, history, dynamic browsing,
direct IDs, backend generation, publish keys, RTMP, and historical
dynamic ancestors
- serialize playback transitions with ban and lifecycle cleanup
transactions and emit realtime outbox events atomically
## Verification
- `make fmt-check`
- `make clippy-check`
- `make nextest-default` (5238 passed, 1596 skipped)
- 15 targeted ignored Testcontainers lifecycle/concurrency tests passed
- `make sqlx-prepare` with only changed offline query cache entries
retained
1 month ago
|
|
|
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();
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
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();
|
refactor: remove low-value thin wrappers (#426)
## Summary
- remove unused compatibility APIs and low-value forwarding wrappers
across the workspace
- inline service, repository, provider, cluster, proxy, and protocol
paths while preserving existing behavior
- remove stale dependencies, metrics, tests, and SQLx metadata generated
for deleted queries
## Validation
- `cargo check --workspace --all-targets`
- `cargo fmt --all -- --check`
- `git diff --check`
- relevant test suites: 1,408 passed, 184 ignored
- broader test run: 2,286 passed
1 month ago
|
|
|
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
|
fix(room): enforce member-owned resource lifecycle (#425)
## Summary
- preserve room membership and owned resources across global bans while
blocking browsing and playback until unban
- clean up member-owned media and dynamic playlists on leave, kick, and
account deletion while preserving ordinary shared playlists
- enforce lifecycle checks for playback, history, dynamic browsing,
direct IDs, backend generation, publish keys, RTMP, and historical
dynamic ancestors
- serialize playback transitions with ban and lifecycle cleanup
transactions and emit realtime outbox events atomically
## Verification
- `make fmt-check`
- `make clippy-check`
- `make nextest-default` (5238 passed, 1596 skipped)
- 15 targeted ignored Testcontainers lifecycle/concurrency tests passed
- `make sqlx-prepare` with only changed offline query cache entries
retained
1 month ago
|
|
|
.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
|
fix(room): enforce member-owned resource lifecycle (#425)
## Summary
- preserve room membership and owned resources across global bans while
blocking browsing and playback until unban
- clean up member-owned media and dynamic playlists on leave, kick, and
account deletion while preserving ordinary shared playlists
- enforce lifecycle checks for playback, history, dynamic browsing,
direct IDs, backend generation, publish keys, RTMP, and historical
dynamic ancestors
- serialize playback transitions with ban and lifecycle cleanup
transactions and emit realtime outbox events atomically
## Verification
- `make fmt-check`
- `make clippy-check`
- `make nextest-default` (5238 passed, 1596 skipped)
- 15 targeted ignored Testcontainers lifecycle/concurrency tests passed
- `make sqlx-prepare` with only changed offline query cache entries
retained
1 month ago
|
|
|
.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
|
fix(room): enforce member-owned resource lifecycle (#425)
## Summary
- preserve room membership and owned resources across global bans while
blocking browsing and playback until unban
- clean up member-owned media and dynamic playlists on leave, kick, and
account deletion while preserving ordinary shared playlists
- enforce lifecycle checks for playback, history, dynamic browsing,
direct IDs, backend generation, publish keys, RTMP, and historical
dynamic ancestors
- serialize playback transitions with ban and lifecycle cleanup
transactions and emit realtime outbox events atomically
## Verification
- `make fmt-check`
- `make clippy-check`
- `make nextest-default` (5238 passed, 1596 skipped)
- 15 targeted ignored Testcontainers lifecycle/concurrency tests passed
- `make sqlx-prepare` with only changed offline query cache entries
retained
1 month ago
|
|
|
.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;
|
|
|
|
|
}
|