121
crates/application/tests/provider/connection_test.rs
Normal file
121
crates/application/tests/provider/connection_test.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::ProviderConnectionQueryPort;
|
||||
use domain::provider::ProviderName;
|
||||
use domain::testing::{FakeCredentialCipher, InMemoryStore, test_user};
|
||||
|
||||
use application::provider::commands::ConnectProviderCommand;
|
||||
use application::provider::use_cases::{connect_provider, disconnect_provider, list_connections};
|
||||
|
||||
const SUBSONIC_CREDENTIAL: &str =
|
||||
r#"{"url":"https://music.example","username":"gabriel","password":"hunter2"}"#;
|
||||
|
||||
fn deps() -> (Arc<InMemoryStore>, connect_provider::Deps) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let deps = connect_provider::Deps {
|
||||
command: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
};
|
||||
(store, deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connecting_stores_the_credential_sealed_rather_than_verbatim() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("alice");
|
||||
|
||||
let cmd = ConnectProviderCommand {
|
||||
user_id: user.id().clone(),
|
||||
provider: ProviderName::new("subsonic").unwrap(),
|
||||
credential: SUBSONIC_CREDENTIAL.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
connect_provider::execute(cmd, &deps).await.unwrap();
|
||||
|
||||
let stored = store.find_by_user(user.id()).await.unwrap();
|
||||
let sealed = stored[0].credential().value();
|
||||
|
||||
assert!(
|
||||
!sealed.windows(7).any(|window| window == b"hunter2"),
|
||||
"the raw password reached storage"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reconnecting_replaces_the_credential_rather_than_adding_a_second() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("bob");
|
||||
let provider = ProviderName::new("subsonic").unwrap();
|
||||
|
||||
for password in ["first", "second"] {
|
||||
let cmd = ConnectProviderCommand {
|
||||
user_id: user.id().clone(),
|
||||
provider: provider.clone(),
|
||||
credential: password.as_bytes().to_vec(),
|
||||
};
|
||||
connect_provider::execute(cmd, &deps).await.unwrap();
|
||||
}
|
||||
|
||||
let stored = store.find_by_user(user.id()).await.unwrap();
|
||||
|
||||
assert_eq!(stored.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn listing_reports_connected_providers_without_their_credentials() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("carol");
|
||||
|
||||
connect_provider::execute(
|
||||
ConnectProviderCommand {
|
||||
user_id: user.id().clone(),
|
||||
provider: ProviderName::new("subsonic").unwrap(),
|
||||
credential: SUBSONIC_CREDENTIAL.as_bytes().to_vec(),
|
||||
},
|
||||
&deps,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let listing = list_connections::execute(
|
||||
user.id().clone(),
|
||||
&list_connections::Deps {
|
||||
query: store.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(listing.len(), 1);
|
||||
assert_eq!(listing[0].provider.value(), "subsonic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disconnecting_removes_the_connection() {
|
||||
let (store, deps) = deps();
|
||||
let user = test_user("dave");
|
||||
let provider = ProviderName::new("subsonic").unwrap();
|
||||
|
||||
connect_provider::execute(
|
||||
ConnectProviderCommand {
|
||||
user_id: user.id().clone(),
|
||||
provider: provider.clone(),
|
||||
credential: SUBSONIC_CREDENTIAL.as_bytes().to_vec(),
|
||||
},
|
||||
&deps,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
disconnect_provider::execute(
|
||||
user.id().clone(),
|
||||
provider,
|
||||
&disconnect_provider::Deps {
|
||||
command: store.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(store.find_by_user(user.id()).await.unwrap().is_empty());
|
||||
}
|
||||
199
crates/application/tests/provider/now_playing_test.rs
Normal file
199
crates/application/tests/provider/now_playing_test.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::provider::ProviderName;
|
||||
use domain::song::{RecordingId, Song};
|
||||
use domain::testing::{
|
||||
FakeCredentialCipher, FakeNowPlaying, FakeRecordingLookup, InMemoryStore, test_user,
|
||||
};
|
||||
|
||||
use application::provider::commands::ConnectProviderCommand;
|
||||
use application::provider::use_cases::{connect_provider, get_now_playing};
|
||||
|
||||
const RECORDING: &str = "f5c7e7a2-0000-4000-8000-000000000001";
|
||||
|
||||
async fn connected_store() -> (Arc<InMemoryStore>, domain::user::UserId) {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("alice");
|
||||
|
||||
connect_provider::execute(
|
||||
ConnectProviderCommand {
|
||||
user_id: user.id().clone(),
|
||||
provider: ProviderName::new("subsonic").unwrap(),
|
||||
credential: br#"{"url":"https://music.example"}"#.to_vec(),
|
||||
},
|
||||
&connect_provider::Deps {
|
||||
command: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(store, user.id().clone())
|
||||
}
|
||||
|
||||
fn song() -> Song {
|
||||
Song::new("Paranoid Android", "Radiohead", None, None).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_playing_song_is_returned_enriched_with_its_recording_id() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding(Some(
|
||||
RecordingId::new(RECORDING).unwrap(),
|
||||
))),
|
||||
};
|
||||
|
||||
let result = get_now_playing::execute(user_id, &deps).await.unwrap();
|
||||
let found = result.unwrap();
|
||||
|
||||
assert_eq!(found.title().value(), "Paranoid Android");
|
||||
assert_eq!(
|
||||
found.recording_id().map(|id| id.value().to_string()),
|
||||
Some(RECORDING.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_song_with_no_musicbrainz_match_is_still_returned() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
let found = get_now_playing::execute(user_id, &deps)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(found.recording_id().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nothing_playing_is_not_an_error() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::silent("subsonic")),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
assert!(
|
||||
get_now_playing::execute(user_id, &deps)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_user_with_no_connection_gets_a_clear_error() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let stranger = test_user("bob");
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
assert!(
|
||||
get_now_playing::execute(stranger.id().clone(), &deps)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_provider_that_cannot_be_reached_offers_nothing_rather_than_failing() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::failing("subsonic", "connection refused")),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
let asked = get_now_playing::execute(user_id, &deps).await;
|
||||
|
||||
assert!(
|
||||
asked.is_ok(),
|
||||
"the user must still be able to type a song by hand: {asked:?}"
|
||||
);
|
||||
assert!(asked.unwrap().is_none(), "nothing found, not an error");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_recording_lookup_that_fails_yields_a_song_with_no_identity() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::failing("musicbrainz is rate limiting")),
|
||||
};
|
||||
|
||||
let playing = get_now_playing::execute(user_id, &deps)
|
||||
.await
|
||||
.expect("a failing enrichment must not fail the lookup")
|
||||
.expect("the song is still playing");
|
||||
|
||||
assert_eq!(playing.title().value(), song().title().value());
|
||||
assert!(
|
||||
playing.recording_id().is_none(),
|
||||
"an identity that could not be found is simply absent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_recording_lookup_that_finds_nothing_yields_a_song_with_no_identity() {
|
||||
let (store, user_id) = connected_store().await;
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
let playing = get_now_playing::execute(user_id, &deps)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(playing.recording_id().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_account_with_no_connection_is_told_so_rather_than_offered_silence() {
|
||||
let store = Arc::new(InMemoryStore::new());
|
||||
let user = test_user("nobody");
|
||||
|
||||
let deps = get_now_playing::Deps {
|
||||
query: store.clone(),
|
||||
cipher: Arc::new(FakeCredentialCipher),
|
||||
now_playing: Arc::new(FakeNowPlaying::playing("subsonic", song())),
|
||||
recordings: Arc::new(FakeRecordingLookup::finding_nothing()),
|
||||
};
|
||||
|
||||
let asked = get_now_playing::execute(user.id().clone(), &deps).await;
|
||||
|
||||
assert!(
|
||||
asked.is_err(),
|
||||
"nothing to connect to is actionable, unlike a provider being down"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user