mod capability;
mod channel_subscription;
mod client;
mod connection;
mod credentials_provider;
mod gateway;
mod listener;
mod participant;
mod session;
mod session_state;
pub use capability::Capability;
pub use client::Client;
pub use gateway::{Gateway, GatewayHandle};
pub use listener::Listener;
use thiserror::Error;
use crate::api_client::FoxgloveApiClientError;
use self::credentials_provider::CredentialsError;
#[derive(Error, Debug)]
pub(super) enum RemoteAccessError {
#[error("Stream error: {0}")]
Stream(livekit::StreamError),
#[error("Room error: {0}")]
Room(livekit::RoomError),
#[error("Authentication error: {0}")]
Auth(FoxgloveApiClientError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl From<livekit::StreamError> for RemoteAccessError {
fn from(error: livekit::StreamError) -> Self {
match error {
livekit::StreamError::Io(e) => RemoteAccessError::Io(e),
other => RemoteAccessError::Stream(other),
}
}
}
impl From<livekit::RoomError> for RemoteAccessError {
fn from(error: livekit::RoomError) -> Self {
RemoteAccessError::Room(error)
}
}
impl From<FoxgloveApiClientError> for RemoteAccessError {
fn from(error: FoxgloveApiClientError) -> Self {
RemoteAccessError::Auth(error)
}
}
impl From<CredentialsError> for RemoteAccessError {
fn from(error: CredentialsError) -> Self {
match error {
CredentialsError::FetchFailed(e) => RemoteAccessError::Auth(e),
}
}
}
impl From<livekit::StreamError> for Box<RemoteAccessError> {
fn from(e: livekit::StreamError) -> Self {
Box::new(RemoteAccessError::from(e))
}
}
impl From<livekit::RoomError> for Box<RemoteAccessError> {
fn from(e: livekit::RoomError) -> Self {
Box::new(RemoteAccessError::from(e))
}
}
impl From<FoxgloveApiClientError> for Box<RemoteAccessError> {
fn from(e: FoxgloveApiClientError) -> Self {
Box::new(RemoteAccessError::from(e))
}
}
impl From<CredentialsError> for Box<RemoteAccessError> {
fn from(e: CredentialsError) -> Self {
Box::new(RemoteAccessError::from(e))
}
}
impl From<std::io::Error> for Box<RemoteAccessError> {
fn from(e: std::io::Error) -> Self {
Box::new(RemoteAccessError::from(e))
}
}
impl RemoteAccessError {
pub(super) fn should_clear_credentials(&self) -> bool {
matches!(
self,
RemoteAccessError::Auth(_) | RemoteAccessError::Room(_)
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_io_error_converts_to_io_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken");
let stream_err = livekit::StreamError::Io(io_err);
let err = RemoteAccessError::from(stream_err);
assert!(
matches!(err, RemoteAccessError::Io(_)),
"StreamError::Io should convert to RemoteAccessError::Io"
);
}
#[test]
fn stream_non_io_error_converts_to_stream_variant() {
let stream_err = livekit::StreamError::AlreadyClosed;
let err = RemoteAccessError::from(stream_err);
assert!(
matches!(err, RemoteAccessError::Stream(_)),
"Non-Io StreamError should convert to RemoteAccessError::Stream"
);
}
#[test]
fn room_error_converts_to_room_variant() {
let room_err = livekit::RoomError::AlreadyClosed;
let err = RemoteAccessError::from(room_err);
assert!(
matches!(err, RemoteAccessError::Room(_)),
"RoomError should convert to RemoteAccessError::Room"
);
}
#[test]
fn io_error_converts_to_io_variant() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err = RemoteAccessError::from(io_err);
assert!(
matches!(err, RemoteAccessError::Io(_)),
"io::Error should convert to RemoteAccessError::Io"
);
}
#[test]
fn should_clear_credentials_for_room_errors() {
let err = RemoteAccessError::Room(livekit::RoomError::AlreadyClosed);
assert!(err.should_clear_credentials());
}
#[test]
fn should_not_clear_credentials_for_io_errors() {
let err = RemoteAccessError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"broken",
));
assert!(!err.should_clear_credentials());
}
#[test]
fn should_not_clear_credentials_for_stream_errors() {
let err = RemoteAccessError::Stream(livekit::StreamError::AlreadyClosed);
assert!(!err.should_clear_credentials());
}
async fn make_api_client_error() -> FoxgloveApiClientError {
use crate::api_client::test_utils::create_test_server;
use crate::api_client::{DeviceToken, FoxgloveApiClientBuilder};
let server = create_test_server().await;
let client = FoxgloveApiClientBuilder::new(DeviceToken::new("bad-token"))
.base_url(server.url())
.build()
.expect("client should build successfully");
match client.fetch_device_info().await {
Err(e) => e,
Ok(_) => panic!("expected fetch_device_info to fail with bad token"),
}
}
#[tokio::test]
async fn api_client_error_converts_to_auth_variant() {
let err = RemoteAccessError::from(make_api_client_error().await);
assert!(
matches!(err, RemoteAccessError::Auth(_)),
"FoxgloveApiClientError should convert to RemoteAccessError::Auth"
);
}
#[tokio::test]
async fn credentials_error_converts_to_auth_variant() {
let api_err = make_api_client_error().await;
let cred_err = CredentialsError::FetchFailed(api_err);
let err = RemoteAccessError::from(cred_err);
assert!(
matches!(err, RemoteAccessError::Auth(_)),
"CredentialsError::FetchFailed should convert to RemoteAccessError::Auth"
);
}
#[tokio::test]
async fn should_clear_credentials_for_auth_errors() {
let err = RemoteAccessError::from(make_api_client_error().await);
assert!(err.should_clear_credentials());
}
}