summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/channels-messages.md5
-rw-r--r--src/channel/app.rs49
-rw-r--r--src/channel/routes/channel/delete.rs9
-rw-r--r--src/channel/routes/channel/test/delete.rs34
4 files changed, 76 insertions, 21 deletions
diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md
index d87a01c..2aa8ac5 100644
--- a/docs/api/channels-messages.md
+++ b/docs/api/channels-messages.md
@@ -164,7 +164,7 @@ This endpoint will respond with a status of `404 Not Found` if the channel ID is
Deletes a channel.
-Deleting a channel prevents it from receiving any further messages, and deletes the messages it contains at that point.
+Deleting a channel prevents it from receiving any further messages. The channel must be empty; to delete a channel with messages in it, delete the messages first (or wait for them to expire).
This endpoint requires the following path parameter:
@@ -190,6 +190,9 @@ The response will have the following fields:
When completed, the service will emit a [message deleted](events.md#message-deleted) event for each message in the channel, followed by a [channel deleted](events.md#channel-deleted) event with the channel's ID.
+### Channel not empty
+
+This endpoint will respond with a status of `409 Conflict` if the channel contains messages.
### Invalid channel ID
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 9a19b16..e32eb6c 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -10,7 +10,7 @@ use crate::{
clock::DateTime,
db::{Duplicate as _, NotFound as _},
event::{repo::Provider as _, Broadcaster, Event, Sequence},
- message::repo::Provider as _,
+ message::{self, repo::Provider as _},
name::{self, Name},
};
@@ -48,38 +48,36 @@ impl<'a> Channels<'a> {
// it exists in the specific moment when you call it.
pub async fn get(&self, channel: &Id) -> Result<Channel, Error> {
let not_found = || Error::NotFound(channel.clone());
+ let deleted = || Error::Deleted(channel.clone());
let mut tx = self.db.begin().await?;
let channel = tx.channels().by_id(channel).await.not_found(not_found)?;
tx.commit().await?;
- channel.as_snapshot().ok_or_else(not_found)
+ channel.as_snapshot().ok_or_else(deleted)
}
- pub async fn delete(&self, channel: &Id, deleted_at: &DateTime) -> Result<(), Error> {
+ pub async fn delete(&self, channel: &Id, deleted_at: &DateTime) -> Result<(), DeleteError> {
let mut tx = self.db.begin().await?;
let channel = tx
.channels()
.by_id(channel)
.await
- .not_found(|| Error::NotFound(channel.clone()))?;
+ .not_found(|| DeleteError::NotFound(channel.clone()))?;
channel
.as_snapshot()
- .ok_or_else(|| Error::Deleted(channel.id().clone()))?;
+ .ok_or_else(|| DeleteError::Deleted(channel.id().clone()))?;
let mut events = Vec::new();
let messages = tx.messages().live(&channel).await?;
- for message in messages {
- let deleted = tx.sequence().next(deleted_at).await?;
- let message = tx.messages().delete(&message, &deleted).await?;
- events.extend(
- message
- .events()
- .filter(Sequence::start_from(deleted.sequence))
- .map(Event::from),
- );
+ let has_messages = messages
+ .iter()
+ .map(message::History::as_snapshot)
+ .any(|message| message.is_some());
+ if has_messages {
+ return Err(DeleteError::NotEmpty(channel.id().clone()));
}
let deleted = tx.sequence().next(deleted_at).await?;
@@ -192,6 +190,29 @@ impl From<LoadError> for Error {
}
#[derive(Debug, thiserror::Error)]
+pub enum DeleteError {
+ #[error("channel {0} not found")]
+ NotFound(Id),
+ #[error("channel {0} deleted")]
+ Deleted(Id),
+ #[error("channel {0} not empty")]
+ NotEmpty(Id),
+ #[error(transparent)]
+ Database(#[from] sqlx::Error),
+ #[error(transparent)]
+ Name(#[from] name::Error),
+}
+
+impl From<LoadError> for DeleteError {
+ fn from(error: LoadError) -> Self {
+ match error {
+ LoadError::Database(error) => error.into(),
+ LoadError::Name(error) => error.into(),
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
pub enum ExpireError {
#[error(transparent)]
Database(#[from] sqlx::Error),
diff --git a/src/channel/routes/channel/delete.rs b/src/channel/routes/channel/delete.rs
index 2d2b5f1..9c093c1 100644
--- a/src/channel/routes/channel/delete.rs
+++ b/src/channel/routes/channel/delete.rs
@@ -36,14 +36,19 @@ impl IntoResponse for Response {
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
-pub struct Error(#[from] pub app::Error);
+pub struct Error(#[from] pub app::DeleteError);
impl IntoResponse for Error {
fn into_response(self) -> response::Response {
let Self(error) = self;
#[allow(clippy::match_wildcard_for_single_variants)]
match error {
- app::Error::NotFound(_) | app::Error::Deleted(_) => NotFound(error).into_response(),
+ app::DeleteError::NotFound(_) | app::DeleteError::Deleted(_) => {
+ NotFound(error).into_response()
+ }
+ app::DeleteError::NotEmpty(_) => {
+ (StatusCode::CONFLICT, error.to_string()).into_response()
+ }
other => Internal::from(other).into_response(),
}
}
diff --git a/src/channel/routes/channel/test/delete.rs b/src/channel/routes/channel/test/delete.rs
index 0371b0a..77a0b03 100644
--- a/src/channel/routes/channel/test/delete.rs
+++ b/src/channel/routes/channel/test/delete.rs
@@ -55,7 +55,7 @@ pub async fn invalid_channel_id() {
// Verify the response
- assert!(matches!(error, app::Error::NotFound(id) if id == channel));
+ assert!(matches!(error, app::DeleteError::NotFound(id) if id == channel));
}
#[tokio::test]
@@ -84,7 +84,7 @@ pub async fn channel_deleted() {
// Verify the response
- assert!(matches!(error, app::Error::Deleted(id) if id == channel.id));
+ assert!(matches!(error, app::DeleteError::Deleted(id) if id == channel.id));
}
#[tokio::test]
@@ -113,7 +113,7 @@ pub async fn channel_expired() {
// Verify the response
- assert!(matches!(error, app::Error::Deleted(id) if id == channel.id));
+ assert!(matches!(error, app::DeleteError::Deleted(id) if id == channel.id));
}
#[tokio::test]
@@ -147,5 +147,31 @@ pub async fn channel_purged() {
// Verify the response
- assert!(matches!(error, app::Error::NotFound(id) if id == channel.id));
+ assert!(matches!(error, app::DeleteError::NotFound(id) if id == channel.id));
+}
+
+#[tokio::test]
+pub async fn channel_not_empty() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+ let sender = fixtures::login::create(&app, &fixtures::now()).await;
+ fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
+
+ // Send the request
+
+ let deleter = fixtures::identity::create(&app, &fixtures::now()).await;
+ let delete::Error(error) = delete::handler(
+ State(app.clone()),
+ Path(channel.id.clone()),
+ fixtures::now(),
+ deleter,
+ )
+ .await
+ .expect_err("deleting a channel with messages fails");
+
+ // Verify the response
+
+ assert!(matches!(error, app::DeleteError::NotEmpty(id) if id == channel.id));
}