diff --git a/Cargo.toml b/Cargo.toml index f5a1f49..e5e1d44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ napi-build = { version = "2.1", optional = true } pyo3-build-config = { version = "0.22", optional = true } [features] -default = [] +default = ["test-e2e"] # extra async-trait = ["dep:async-trait"] serialize = ["dep:serde", "uuid/serde"] diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index e862322..d1ba78d 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -121,7 +121,7 @@ impl Workspace { })?; self.callback(move |controller: Workspace| { tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error - // If it blocks the main thread too many time we have to change this + // If it blocks the main thread too many time we have to change this }); Ok(()) diff --git a/src/tests/client.rs b/src/tests/client.rs new file mode 100644 index 0000000..0e015ab --- /dev/null +++ b/src/tests/client.rs @@ -0,0 +1,134 @@ +use crate::api::{AsyncReceiver, AsyncSender}; +use super::{assert_or_err, fixtures::{ScopedFixture, WorkspaceFixture}}; + +#[tokio::test] +async fn test_buffer_search() { + WorkspaceFixture::one("alice") + .with(|(_, workspace_alice): &mut (crate::Client, crate::Workspace)| { + let buffer_name = uuid::Uuid::new_v4().to_string(); + let workspace_alice = workspace_alice.clone(); + + async move { + workspace_alice.create_buffer(&buffer_name).await?; + assert_or_err!(!workspace_alice + .search_buffers(Some(&buffer_name[0..4])) + .is_empty()); + assert_or_err!(workspace_alice.search_buffers(Some("_")).is_empty()); + workspace_alice.delete_buffer(&buffer_name).await?; + Ok(()) + } + }) + .await; +} + +#[tokio::test] +async fn test_send_operation() { + WorkspaceFixture::two("alice", "bob") + .with(|((_, workspace_alice), (_, workspace_bob))| { + let buffer_name = uuid::Uuid::new_v4().to_string(); + let workspace_alice = workspace_alice.clone(); + let workspace_bob = workspace_bob.clone(); + + async move { + workspace_alice.create_buffer(&buffer_name).await?; + let alice = workspace_alice.attach_buffer(&buffer_name).await?; + let bob = workspace_bob.attach_buffer(&buffer_name).await?; + + alice.send(crate::api::TextChange { + start_idx: 0, + end_idx: 0, + content: "hello world".to_string(), + })?; + + let result = bob.recv().await?; + assert_or_err!(result.change.start_idx == 0); + assert_or_err!(result.change.end_idx == 0); + assert_or_err!(result.change.content == "hello world"); + + Ok(()) + } + }) + .await; +} + +#[tokio::test] +async fn test_content_converges() { + WorkspaceFixture::two("alice", "bob") + .with(|((_, workspace_alice), (_, workspace_bob))| { + let buffer_name = uuid::Uuid::new_v4().to_string(); + let workspace_alice = workspace_alice.clone(); + let workspace_bob = workspace_bob.clone(); + + async move { + workspace_alice.create_buffer(&buffer_name).await?; + let alice = workspace_alice.attach_buffer(&buffer_name).await?; + let bob = workspace_bob.attach_buffer(&buffer_name).await?; + + let mut join_set = tokio::task::JoinSet::new(); + + let _alice = alice.clone(); + join_set.spawn(async move { + for i in 0..10 { + _alice.content().await?; + _alice.send(crate::api::TextChange { + start_idx: 7 * i, + end_idx: 7 * i, + content: format!("alice{i} "), // TODO generate a random string instead!! + })?; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Ok::<(), crate::errors::ControllerError>(()) + }); + + let _bob = bob.clone(); + join_set.spawn(async move { + for i in 0..10 { + _bob.content().await?; + _bob.send(crate::api::TextChange { + start_idx: 5 * i, + end_idx: 5 * i, + content: format!("bob{i} "), // TODO generate a random string instead!! + })?; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Ok::<(), crate::errors::ControllerError>(()) + }); + + while let Some(x) = join_set.join_next().await { + x??; + } + + // TODO is there a nicer way to make sure we received all changes? + + for i in 0..100 { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + match bob.try_recv().await? { + Some(change) => bob.ack(change.version), + None => break, + } + eprintln!("bob more to recv at attempt #{i}"); + } + + for i in 0..100 { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + match alice.try_recv().await? { + Some(change) => alice.ack(change.version), + None => break, + } + eprintln!("alice more to recv at attempt #{i}"); + } + + let alice_content = alice.content().await?; + let bob_content = bob.content().await?; + + eprintln!("alice: {alice_content}"); + eprintln!("bob : {bob_content}"); + + assert_or_err!(alice_content == bob_content); + assert_or_err!(false); + + Ok(()) + } + }) + .await; +} diff --git a/src/tests/e2e.rs b/src/tests/e2e.rs deleted file mode 100644 index e2ba7d2..0000000 --- a/src/tests/e2e.rs +++ /dev/null @@ -1,417 +0,0 @@ -use std::{error::Error, fmt::Display, future::Future}; - -use crate::api::{AsyncReceiver, AsyncSender}; - -#[derive(Debug)] -pub struct AssertionError(String); -impl AssertionError { - fn new(msg: &str) -> Self { - Self(msg.to_string()) - } -} -impl Display for AssertionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} -impl std::error::Error for AssertionError {} - -#[allow(async_fn_in_trait)] -pub trait ScopedFixture { - async fn setup(&mut self) -> Result>; - - async fn cleanup(&mut self, resource: Option) { - drop(resource) - } - - async fn inner_with(mut self, cb: impl FnOnce(&mut T) -> F) -> Result<(), Box> - where - Self: Sized, - F: Future>>, - { - match self.setup().await { - Ok(mut t) => { - let res = cb(&mut t).await; - self.cleanup(Some(t)).await; - res - } - Err(e) => { - self.cleanup(None).await; - Err(e) - } - } - } - - async fn with(self, cb: impl FnOnce(&mut T) -> F) - where - Self: Sized, - F: Future>>, - { - if let Err(e) = self.inner_with(cb).await { - panic!("{e}"); - } - } -} - -macro_rules! assert_or_err { - ($s:expr) => { - if !$s { - return Err(AssertionError::new(&format!( - "assertion failed at line {}: {}", - std::line!(), - stringify!($s) - )) - .into()); - } - }; - ($s:expr, $msg:literal) => { - if !$s { - return Err(AssertionError::new($msg).into()); - } - }; -} - -struct ClientFixture { - name: String, - username: Option, - password: Option, -} -impl ClientFixture { - fn of(name: &str) -> Self { - Self { - name: name.to_string(), - username: None, - password: None, - } - } -} - -impl ScopedFixture for ClientFixture { - async fn setup(&mut self) -> Result> { - let upper = self.name.to_uppercase(); - let username = self.username.clone().unwrap_or_else(|| { - std::env::var(format!("CODEMP_TEST_USERNAME_{upper}")).unwrap_or_default() - }); - let password = self.password.clone().unwrap_or_else(|| { - std::env::var(format!("CODEMP_TEST_PASSWORD_{upper}")).unwrap_or_default() - }); - let client = crate::Client::connect(crate::api::Config { - username, - password, - tls: Some(false), - ..Default::default() - }) - .await?; - - Ok(client) - } -} - -struct WorkspaceFixture { - user: String, - invite: Option, - workspace: String, -} -impl WorkspaceFixture { - #[allow(unused)] - fn of(user: &str, invite: &str, workspace: &str) -> Self { - Self { - user: user.to_string(), - invite: Some(invite.to_string()), - workspace: workspace.to_string(), - } - } - - fn any(user: &str) -> Self { - Self { - user: user.to_string(), - invite: None, - workspace: uuid::Uuid::new_v4().to_string(), - } - } - - fn two(user: &str, invite: &str) -> Self { - Self { - user: user.to_string(), - invite: Some(invite.to_string()), - workspace: uuid::Uuid::new_v4().to_string(), - } - } -} - -impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { - async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { - let client = ClientFixture::of(&self.user).setup().await?; - client.create_workspace(&self.workspace).await?; - let workspace = client.attach_workspace(&self.workspace).await?; - Ok((client, workspace)) - } - - async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { - if let Some((client, _workspace)) = resource { - client.leave_workspace(&self.workspace); - if let Err(e) = client.delete_workspace(&self.workspace).await { - eprintln!("could not delete workspace: {e}"); - } - } - } -} - -impl ScopedFixture<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace))> for WorkspaceFixture { - async fn setup(&mut self) -> Result<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace)), Box> { - let client = ClientFixture::of(&self.user).setup().await?; - let invite_client = ClientFixture::of( - &self - .invite - .clone() - .unwrap_or(uuid::Uuid::new_v4().to_string()), - ) - .setup() - .await?; - client.create_workspace(&self.workspace).await?; - client - .invite_to_workspace(&self.workspace, invite_client.current_user().name.clone()) - .await?; - let workspace = client.attach_workspace(&self.workspace).await?; - let invite = invite_client.attach_workspace(&self.workspace).await?; - Ok(((client, workspace), (invite_client, invite))) - } - - async fn cleanup(&mut self, resource: Option<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace))>) { - if let Some(((client, _workspace), (_, _))) = resource { - client.leave_workspace(&self.workspace); - if let Err(e) = client.delete_workspace(&self.workspace).await { - eprintln!("could not delete workspace: {e}"); - } - } - } -} - -#[tokio::test] -async fn test_workspace_interactions() { - if let Err(e) = async { - let client_alice = ClientFixture::of("alice").setup().await?; - let client_bob = ClientFixture::of("bob").setup().await?; - let workspace_name = uuid::Uuid::new_v4().to_string(); - - client_alice.create_workspace(&workspace_name).await?; - let owned_workspaces = client_alice.fetch_owned_workspaces().await?; - assert_or_err!(owned_workspaces.contains(&workspace_name)); - client_alice.attach_workspace(&workspace_name).await?; - assert_or_err!(vec![workspace_name.clone()] == client_alice.active_workspaces()); - - client_alice - .invite_to_workspace(&workspace_name, &client_bob.current_user().name) - .await?; - client_bob.attach_workspace(&workspace_name).await?; - assert_or_err!(client_bob - .fetch_joined_workspaces() - .await? - .contains(&workspace_name)); - - assert_or_err!(client_bob.leave_workspace(&workspace_name)); - assert_or_err!(client_alice.leave_workspace(&workspace_name)); - - client_alice.delete_workspace(&workspace_name).await?; - - Ok::<(), Box>(()) - } - .await - { - panic!("{e}"); - } -} - -// ======= - -#[tokio::test] -async fn cannot_delete_others_workspaces() { - WorkspaceFixture::two("alice", "bob") - .with(|((_, ws_alice), (client_bob, _))| { - let ws_alice = ws_alice.clone(); - let client_bob = client_bob.clone(); - async move { - assert_or_err!( - client_bob.delete_workspace(&ws_alice.id()).await.is_err(), - "bob was allowed to delete a workspace he didn't own!" - ); - Ok(()) - } - - }) - .await -} - -#[tokio::test] -async fn test_cant_create_buffer_twice() { - WorkspaceFixture::any("alice") - .with(|(_, ws): &mut (crate::Client, crate::Workspace)| { - let ws = ws.clone(); - async move { - ws.create_buffer("cacca").await?; - assert!( - ws.create_buffer("cacca").await.is_err(), - "alice could create again the same buffer" - ); - Ok(()) - } - }) - .await; -} - -#[tokio::test] -async fn test_buffer_search() { - WorkspaceFixture::two("alice", "bob") - .with(|((_, workspace_alice), (_, workspace_bob))| { - let buffer_name = uuid::Uuid::new_v4().to_string(); - let workspace_alice = workspace_alice.clone(); - let workspace_bob = workspace_bob.clone(); - - async move { - workspace_alice.create_buffer(&buffer_name).await?; - assert_or_err!(vec![buffer_name.clone()] == workspace_alice.fetch_buffers().await?); - assert_or_err!(vec![buffer_name.clone()] == workspace_bob.fetch_buffers().await?); - - assert_or_err!(!workspace_alice - .search_buffers(Some(&buffer_name[0..4])) - .is_empty()); - assert_or_err!(workspace_alice.search_buffers(Some("_")).is_empty()); - - workspace_alice.delete_buffer(&buffer_name).await?; - - Ok(()) - } - }) - .await; -} - -#[tokio::test] -async fn cannot_delete_others_buffers() { - WorkspaceFixture::two("alice", "bob") - .with(|((_, workspace_alice), (_, workspace_bob))| { - let buffer_name = uuid::Uuid::new_v4().to_string(); - let workspace_alice = workspace_alice.clone(); - let workspace_bob = workspace_bob.clone(); - - async move { - workspace_alice.create_buffer(&buffer_name).await?; - assert_or_err!(workspace_bob.delete_buffer(&buffer_name).await.is_err()); - Ok(()) - } - }) - .await; -} - -// ===== - -#[tokio::test] -async fn test_send_operation() { - WorkspaceFixture::two("alice", "bob") - .with(|((_, workspace_alice), (_, workspace_bob))| { - let buffer_name = uuid::Uuid::new_v4().to_string(); - let workspace_alice = workspace_alice.clone(); - let workspace_bob = workspace_bob.clone(); - - async move { - workspace_alice.create_buffer(&buffer_name).await?; - let alice = workspace_alice.attach_buffer(&buffer_name).await?; - let bob = workspace_bob.attach_buffer(&buffer_name).await?; - - alice.send(crate::api::TextChange { - start_idx: 0, - end_idx: 0, - content: "hello world".to_string(), - })?; - - let result = bob.recv().await?; - assert_or_err!(result.change.start_idx == 0); - assert_or_err!(result.change.end_idx == 0); - assert_or_err!(result.change.content == "hello world"); - - Ok(()) - } - }) - .await; -} - -#[tokio::test] -async fn test_content_converges() { - WorkspaceFixture::two("alice", "bob") - .with(|((_, workspace_alice), (_, workspace_bob))| { - let buffer_name = uuid::Uuid::new_v4().to_string(); - let workspace_alice = workspace_alice.clone(); - let workspace_bob = workspace_bob.clone(); - - async move { - workspace_alice.create_buffer(&buffer_name).await?; - let alice = workspace_alice.attach_buffer(&buffer_name).await?; - let bob = workspace_bob.attach_buffer(&buffer_name).await?; - - let mut join_set = tokio::task::JoinSet::new(); - - let _alice = alice.clone(); - join_set.spawn(async move { - for i in 0..10 { - _alice.content().await?; - _alice.send(crate::api::TextChange { - start_idx: 7 * i, - end_idx: 7 * i, - content: format!("alice{i} "), // TODO generate a random string instead!! - })?; - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - Ok::<(), crate::errors::ControllerError>(()) - }); - - let _bob = bob.clone(); - join_set.spawn(async move { - for i in 0..10 { - _bob.content().await?; - _bob.send(crate::api::TextChange { - start_idx: 5 * i, - end_idx: 5 * i, - content: format!("bob{i} "), // TODO generate a random string instead!! - })?; - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - Ok::<(), crate::errors::ControllerError>(()) - }); - - while let Some(x) = join_set.join_next().await { - x??; - } - - // TODO is there a nicer way to make sure we received all changes? - - for i in 0..100 { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - match bob.try_recv().await? { - Some(change) => bob.ack(change.version), - None => break, - } - eprintln!("bob more to recv at attempt #{i}"); - } - - for i in 0..100 { - tokio::time::sleep(std::time::Duration::from_millis(50)).await; - match alice.try_recv().await? { - Some(change) => alice.ack(change.version), - None => break, - } - eprintln!("alice more to recv at attempt #{i}"); - } - - let alice_content = alice.content().await?; - let bob_content = bob.content().await?; - - eprintln!("alice: {alice_content}"); - eprintln!("bob : {bob_content}"); - - assert_or_err!(alice_content == bob_content); - assert_or_err!(false); - - Ok(()) - } - }) - .await; -} diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs new file mode 100644 index 0000000..9f2aebc --- /dev/null +++ b/src/tests/fixtures.rs @@ -0,0 +1,155 @@ +use std::{error::Error, future::Future}; + +// TODO create a BufferFixture too + +#[allow(async_fn_in_trait)] +pub trait ScopedFixture { + async fn setup(&mut self) -> Result>; + + async fn cleanup(&mut self, resource: Option) { + drop(resource) + } + + async fn inner_with(mut self, cb: impl FnOnce(&mut T) -> F) -> Result<(), Box> + where + Self: Sized, + F: Future>>, + { + match self.setup().await { + Ok(mut t) => { + let res = cb(&mut t).await; + self.cleanup(Some(t)).await; + res + } + Err(e) => { + self.cleanup(None).await; + Err(e) + } + } + } + + async fn with(self, cb: impl FnOnce(&mut T) -> F) + where + Self: Sized, + F: Future>>, + { + if let Err(e) = self.inner_with(cb).await { + panic!("{e}"); + } + } +} + +pub struct ClientFixture { + name: String, + username: Option, + password: Option, +} +impl ClientFixture { + pub fn of(name: &str) -> Self { + Self { + name: name.to_string(), + username: None, + password: None, + } + } +} + +impl ScopedFixture for ClientFixture { + async fn setup(&mut self) -> Result> { + let upper = self.name.to_uppercase(); + let username = self.username.clone().unwrap_or_else(|| { + std::env::var(format!("CODEMP_TEST_USERNAME_{upper}")).unwrap_or_default() + }); + let password = self.password.clone().unwrap_or_else(|| { + std::env::var(format!("CODEMP_TEST_PASSWORD_{upper}")).unwrap_or_default() + }); + let client = crate::Client::connect(crate::api::Config { + username, + password, + tls: Some(false), + ..Default::default() + }) + .await?; + + Ok(client) + } +} + +pub struct WorkspaceFixture { + user: String, + invite: Option, + workspace: String, +} +impl WorkspaceFixture { + pub fn of(user: &str, invite: &str, workspace: &str) -> Self { + Self { + user: user.to_string(), + invite: Some(invite.to_string()), + workspace: workspace.to_string(), + } + } + + pub fn one(user: &str) -> Self { + Self { + user: user.to_string(), + invite: None, + workspace: uuid::Uuid::new_v4().to_string(), + } + } + + pub fn two(user: &str, invite: &str) -> Self { + Self { + user: user.to_string(), + invite: Some(invite.to_string()), + workspace: uuid::Uuid::new_v4().to_string(), + } + } +} + +impl ScopedFixture<(crate::Client, crate::Workspace)> for WorkspaceFixture { + async fn setup(&mut self) -> Result<(crate::Client, crate::Workspace), Box> { + let client = ClientFixture::of(&self.user).setup().await?; + client.create_workspace(&self.workspace).await?; + let workspace = client.attach_workspace(&self.workspace).await?; + Ok((client, workspace)) + } + + async fn cleanup(&mut self, resource: Option<(crate::Client, crate::Workspace)>) { + if let Some((client, _workspace)) = resource { + client.leave_workspace(&self.workspace); + if let Err(e) = client.delete_workspace(&self.workspace).await { + eprintln!("could not delete workspace: {e}"); + } + } + } +} + +impl ScopedFixture<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace))> for WorkspaceFixture { + async fn setup(&mut self) -> Result<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace)), Box> { + let client = ClientFixture::of(&self.user).setup().await?; + let invite_client = ClientFixture::of( + &self + .invite + .clone() + .unwrap_or(uuid::Uuid::new_v4().to_string()), + ) + .setup() + .await?; + client.create_workspace(&self.workspace).await?; + client + .invite_to_workspace(&self.workspace, invite_client.current_user().name.clone()) + .await?; + let workspace = client.attach_workspace(&self.workspace).await?; + let invite = invite_client.attach_workspace(&self.workspace).await?; + Ok(((client, workspace), (invite_client, invite))) + } + + async fn cleanup(&mut self, resource: Option<((crate::Client, crate::Workspace), (crate::Client, crate::Workspace))>) { + if let Some(((client, _workspace), (_, _))) = resource { + client.leave_workspace(&self.workspace); + if let Err(e) = client.delete_workspace(&self.workspace).await { + eprintln!("could not delete workspace: {e}"); + } + } + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index f65a2ca..a1907ca 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,2 +1,45 @@ -#[cfg(feature = "test-e2e")] -pub mod e2e; +#[cfg(all(test, feature = "test-e2e"))] +mod client; + +#[cfg(all(test, feature = "test-e2e"))] +mod server; + +pub mod fixtures; + +#[derive(Debug)] +pub struct AssertionError(String); + +impl AssertionError { + pub fn new(msg: &str) -> Self { + Self(msg.to_string()) + } +} + +impl std::fmt::Display for AssertionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for AssertionError {} + +#[macro_export] +macro_rules! assert_or_err { + ($s:expr) => { + if !$s { + return Err($crate::tests::AssertionError::new(&format!( + "assertion failed at line {}: {}", + std::line!(), + stringify!($s) + )) + .into()); + } + }; + ($s:expr, $msg:literal) => { + if !$s { + return Err($crate::tests::AssertionError::new($msg).into()); + } + }; +} + +pub use assert_or_err; diff --git a/src/tests/server.rs b/src/tests/server.rs new file mode 100644 index 0000000..070db87 --- /dev/null +++ b/src/tests/server.rs @@ -0,0 +1,106 @@ +use super::{assert_or_err, fixtures::{ClientFixture, ScopedFixture, WorkspaceFixture}}; + +#[tokio::test] +async fn cannot_delete_others_workspaces() { + WorkspaceFixture::two("alice", "bob") + .with(|((_, ws_alice), (client_bob, _))| { + let ws_alice = ws_alice.clone(); + let client_bob = client_bob.clone(); + async move { + assert_or_err!( + client_bob.delete_workspace(&ws_alice.id()).await.is_err(), + "bob was allowed to delete a workspace he didn't own!" + ); + Ok(()) + } + + }) + .await +} + +#[tokio::test] +async fn test_buffer_create() { + WorkspaceFixture::one("alice") + .with(|(_, workspace_alice): &mut (crate::Client, crate::Workspace)| { + let buffer_name = uuid::Uuid::new_v4().to_string(); + let workspace_alice = workspace_alice.clone(); + + async move { + workspace_alice.create_buffer(&buffer_name).await?; + assert_or_err!(vec![buffer_name.clone()] == workspace_alice.fetch_buffers().await?); + workspace_alice.delete_buffer(&buffer_name).await?; + + Ok(()) + } + }) + .await; +} + +#[tokio::test] +async fn test_cant_create_buffer_twice() { + WorkspaceFixture::one("alice") + .with(|(_, ws): &mut (crate::Client, crate::Workspace)| { + let ws = ws.clone(); + async move { + ws.create_buffer("cacca").await?; + assert!( + ws.create_buffer("cacca").await.is_err(), + "alice could create again the same buffer" + ); + Ok(()) + } + }) + .await; +} + +#[tokio::test] +async fn cannot_delete_others_buffers() { + WorkspaceFixture::two("alice", "bob") + .with(|((_, workspace_alice), (_, workspace_bob))| { + let buffer_name = uuid::Uuid::new_v4().to_string(); + let workspace_alice = workspace_alice.clone(); + let workspace_bob = workspace_bob.clone(); + + async move { + workspace_alice.create_buffer(&buffer_name).await?; + assert_or_err!(workspace_bob.delete_buffer(&buffer_name).await.is_err()); + Ok(()) + } + }) + .await; +} + +#[tokio::test] // TODO split down this test in smaller checks +async fn test_workspace_interactions() { + if let Err(e) = async { + let client_alice = ClientFixture::of("alice").setup().await?; + let client_bob = ClientFixture::of("bob").setup().await?; + let workspace_name = uuid::Uuid::new_v4().to_string(); + + client_alice.create_workspace(&workspace_name).await?; + let owned_workspaces = client_alice.fetch_owned_workspaces().await?; + assert_or_err!(owned_workspaces.contains(&workspace_name)); + client_alice.attach_workspace(&workspace_name).await?; + assert_or_err!(vec![workspace_name.clone()] == client_alice.active_workspaces()); + + client_alice + .invite_to_workspace(&workspace_name, &client_bob.current_user().name) + .await?; + client_bob.attach_workspace(&workspace_name).await?; + assert_or_err!(client_bob + .fetch_joined_workspaces() + .await? + .contains(&workspace_name)); + + assert_or_err!(client_bob.leave_workspace(&workspace_name)); + assert_or_err!(client_alice.leave_workspace(&workspace_name)); + + client_alice.delete_workspace(&workspace_name).await?; + + Ok::<(), Box>(()) + } + .await + { + panic!("{e}"); + } +}