summaryrefslogtreecommitdiff
path: root/src/channel
diff options
context:
space:
mode:
Diffstat (limited to 'src/channel')
-rw-r--r--src/channel/app.rs2
-rw-r--r--src/channel/routes.rs19
-rw-r--r--src/channel/routes/test/list.rs64
-rw-r--r--src/channel/routes/test/mod.rs3
-rw-r--r--src/channel/routes/test/on_create.rs58
-rw-r--r--src/channel/routes/test/on_send.rs148
6 files changed, 286 insertions, 8 deletions
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 48e3e3c..3c92d76 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -78,7 +78,7 @@ impl<'a> Channels<'a> {
channel: &channel::Id,
subscribed_at: &DateTime,
resume_at: Option<&str>,
- ) -> Result<impl Stream<Item = broadcast::Message>, EventsError> {
+ ) -> Result<impl Stream<Item = broadcast::Message> + std::fmt::Debug, EventsError> {
// Somewhat arbitrarily, expire after 90 days.
let expire_at = subscribed_at.to_owned() - TimeDelta::days(90);
diff --git a/src/channel/routes.rs b/src/channel/routes.rs
index 383ec58..674c876 100644
--- a/src/channel/routes.rs
+++ b/src/channel/routes.rs
@@ -17,14 +17,17 @@ use crate::{
},
};
+#[cfg(test)]
+mod test;
+
pub fn router() -> Router<App> {
Router::new()
- .route("/api/channels", get(list_channels))
+ .route("/api/channels", get(list))
.route("/api/channels", post(on_create))
.route("/api/channels/:channel", post(on_send))
}
-async fn list_channels(State(app): State<App>, _: Login) -> Result<Channels, InternalError> {
+async fn list(State(app): State<App>, _: Login) -> Result<Channels, InternalError> {
let channels = app.channels().all().await?;
let response = Channels(channels);
@@ -40,7 +43,7 @@ impl IntoResponse for Channels {
}
}
-#[derive(serde::Deserialize)]
+#[derive(Clone, serde::Deserialize)]
struct CreateRequest {
name: String,
}
@@ -59,6 +62,7 @@ async fn on_create(
Ok(Json(channel))
}
+#[derive(Debug)]
struct CreateError(app::CreateError);
impl IntoResponse for CreateError {
@@ -73,20 +77,20 @@ impl IntoResponse for CreateError {
}
}
-#[derive(serde::Deserialize)]
+#[derive(Clone, serde::Deserialize)]
struct SendRequest {
message: String,
}
async fn on_send(
+ State(app): State<App>,
Path(channel): Path<channel::Id>,
RequestedAt(sent_at): RequestedAt,
- State(app): State<App>,
login: Login,
- Json(form): Json<SendRequest>,
+ Json(request): Json<SendRequest>,
) -> Result<StatusCode, ErrorResponse> {
app.channels()
- .send(&login, &channel, &form.message, &sent_at)
+ .send(&login, &channel, &request.message, &sent_at)
.await
// Could impl `From` here, but it's more code and this is used once.
.map_err(ErrorResponse)?;
@@ -94,6 +98,7 @@ async fn on_send(
Ok(StatusCode::ACCEPTED)
}
+#[derive(Debug)]
struct ErrorResponse(EventsError);
impl IntoResponse for ErrorResponse {
diff --git a/src/channel/routes/test/list.rs b/src/channel/routes/test/list.rs
new file mode 100644
index 0000000..f7f7b44
--- /dev/null
+++ b/src/channel/routes/test/list.rs
@@ -0,0 +1,64 @@
+use axum::extract::State;
+
+use crate::{channel::routes, test::fixtures};
+
+#[tokio::test]
+async fn empty_list() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let viewer = fixtures::login::create(&app).await;
+
+ // Call the endpoint
+
+ let routes::Channels(channels) = routes::list(State(app), viewer)
+ .await
+ .expect("always succeeds");
+
+ // Verify the semantics
+
+ assert!(channels.is_empty());
+}
+
+#[tokio::test]
+async fn one_channel() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let viewer = fixtures::login::create(&app).await;
+ let channel = fixtures::channel::create(&app).await;
+
+ // Call the endpoint
+
+ let routes::Channels(channels) = routes::list(State(app), viewer)
+ .await
+ .expect("always succeeds");
+
+ // Verify the semantics
+
+ assert!(channels.contains(&channel));
+}
+
+#[tokio::test]
+async fn multiple_channels() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let viewer = fixtures::login::create(&app).await;
+ let channels = vec![
+ fixtures::channel::create(&app).await,
+ fixtures::channel::create(&app).await,
+ ];
+
+ // Call the endpoint
+
+ let routes::Channels(response_channels) = routes::list(State(app), viewer)
+ .await
+ .expect("always succeeds");
+
+ // Verify the semantics
+
+ assert!(channels
+ .into_iter()
+ .all(|channel| response_channels.contains(&channel)));
+}
diff --git a/src/channel/routes/test/mod.rs b/src/channel/routes/test/mod.rs
new file mode 100644
index 0000000..ab663eb
--- /dev/null
+++ b/src/channel/routes/test/mod.rs
@@ -0,0 +1,3 @@
+mod list;
+mod on_create;
+mod on_send;
diff --git a/src/channel/routes/test/on_create.rs b/src/channel/routes/test/on_create.rs
new file mode 100644
index 0000000..df23deb
--- /dev/null
+++ b/src/channel/routes/test/on_create.rs
@@ -0,0 +1,58 @@
+use axum::extract::{Json, State};
+
+use crate::{
+ channel::{app, routes},
+ test::fixtures,
+};
+
+#[tokio::test]
+async fn new_channel() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let creator = fixtures::login::create(&app).await;
+
+ // Call the endpoint
+
+ let name = fixtures::channel::propose();
+ let request = routes::CreateRequest { name };
+ let Json(response_channel) =
+ routes::on_create(State(app.clone()), creator, Json(request.clone()))
+ .await
+ .expect("new channel in an empty app");
+
+ // Verify the structure of the response
+
+ assert_eq!(request.name, response_channel.name);
+
+ // Verify the semantics
+
+ let channels = app.channels().all().await.expect("always succeeds");
+
+ assert!(channels.contains(&response_channel));
+}
+
+#[tokio::test]
+async fn duplicate_name() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let creator = fixtures::login::create(&app).await;
+ let channel = fixtures::channel::create(&app).await;
+
+ // Call the endpoint
+
+ let request = routes::CreateRequest { name: channel.name };
+ let routes::CreateError(error) =
+ routes::on_create(State(app.clone()), creator, Json(request.clone()))
+ .await
+ .expect_err("duplicate channel name");
+
+ // Verify the structure of the response
+
+ fixtures::error::expected!(
+ error,
+ app::CreateError::DuplicateName(name),
+ assert_eq!(request.name, name),
+ );
+}
diff --git a/src/channel/routes/test/on_send.rs b/src/channel/routes/test/on_send.rs
new file mode 100644
index 0000000..eab7c32
--- /dev/null
+++ b/src/channel/routes/test/on_send.rs
@@ -0,0 +1,148 @@
+use axum::{
+ extract::{Json, Path, State},
+ http::StatusCode,
+};
+use futures::stream::StreamExt;
+
+use crate::{
+ channel::{app, routes},
+ repo::channel,
+ test::fixtures::{self, future::Immediately as _},
+};
+
+#[tokio::test]
+async fn channel_exists() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::login::create(&app).await;
+ let channel = fixtures::channel::create(&app).await;
+
+ // Call the endpoint
+
+ let sent_at = fixtures::now();
+ let request = routes::SendRequest {
+ message: fixtures::message::propose(),
+ };
+ let status = routes::on_send(
+ State(app.clone()),
+ Path(channel.id.clone()),
+ sent_at.clone(),
+ sender.clone(),
+ Json(request.clone()),
+ )
+ .await
+ .expect("sending to a valid channel");
+
+ // Verify the structure of the response
+
+ assert_eq!(StatusCode::ACCEPTED, status);
+
+ // Verify the semantics
+
+ let subscribed_at = fixtures::now();
+ let mut events = app
+ .channels()
+ .events(&channel.id, &subscribed_at, None)
+ .await
+ .expect("subscribing to a valid channel");
+
+ let event = events
+ .next()
+ .immediately()
+ .await
+ .expect("event received by subscribers");
+
+ assert_eq!(request.message, event.body);
+ assert_eq!(sender, event.sender);
+ assert_eq!(*sent_at, event.sent_at);
+}
+
+#[tokio::test]
+async fn messages_in_order() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::login::create(&app).await;
+ let channel = fixtures::channel::create(&app).await;
+
+ // Call the endpoint (twice)
+
+ let requests = vec![
+ (
+ fixtures::now(),
+ routes::SendRequest {
+ message: fixtures::message::propose(),
+ },
+ ),
+ (
+ fixtures::now(),
+ routes::SendRequest {
+ message: fixtures::message::propose(),
+ },
+ ),
+ ];
+
+ for (sent_at, request) in &requests {
+ routes::on_send(
+ State(app.clone()),
+ Path(channel.id.clone()),
+ sent_at.clone(),
+ sender.clone(),
+ Json(request.clone()),
+ )
+ .await
+ .expect("sending to a valid channel");
+ }
+
+ // Verify the semantics
+
+ let subscribed_at = fixtures::now();
+ let events = app
+ .channels()
+ .events(&channel.id, &subscribed_at, None)
+ .await
+ .expect("subscribing to a valid channel")
+ .take(requests.len());
+
+ let events = events.collect::<Vec<_>>().immediately().await;
+
+ for ((sent_at, request), event) in requests.into_iter().zip(events) {
+ assert_eq!(request.message, event.body);
+ assert_eq!(sender, event.sender);
+ assert_eq!(*sent_at, event.sent_at);
+ }
+}
+
+#[tokio::test]
+async fn nonexistent_channel() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let login = fixtures::login::create(&app).await;
+
+ // Call the endpoint
+
+ let sent_at = fixtures::now();
+ let channel = channel::Id::generate();
+ let request = routes::SendRequest {
+ message: fixtures::message::propose(),
+ };
+ let routes::ErrorResponse(error) = routes::on_send(
+ State(app),
+ Path(channel.clone()),
+ sent_at,
+ login,
+ Json(request),
+ )
+ .await
+ .expect_err("sending to a nonexistent channel");
+
+ // Verify the structure of the response
+
+ fixtures::error::expected!(
+ error,
+ app::EventsError::ChannelNotFound(error_channel),
+ assert_eq!(channel, error_channel)
+ );
+}