summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/api/channels-messages.md14
-rw-r--r--src/channel/app.rs8
-rw-r--r--src/channel/mod.rs1
-rw-r--r--src/channel/routes/post.rs3
-rw-r--r--src/channel/routes/test.rs24
-rw-r--r--src/channel/validate.rs23
-rw-r--r--src/test/fixtures/channel.rs5
7 files changed, 76 insertions, 2 deletions
diff --git a/docs/api/channels-messages.md b/docs/api/channels-messages.md
index 9854d22..6391b5a 100644
--- a/docs/api/channels-messages.md
+++ b/docs/api/channels-messages.md
@@ -64,6 +64,14 @@ The request must have the following fields:
|:-------|:-------|:--|
| `name` | string | The channel's name. |
+The proposed `name` must be valid. The precise definition of valid is still up in the air, but, at minimum:
+
+* It must be non-empty.
+* It must not be "too long." (Currently, 64 characters is too long.)
+* It must begin with a printing character.
+* It must end with a printing character.
+* It must not contain runs of multiple whitespace characters.
+
### Success
This endpoint will respond with a status of `202 Accepted` when successful. The body of the response will be a JSON object describing the new channel:
@@ -86,7 +94,11 @@ The returned name may not be identical to the name requested, as the name will b
When completed, the service will emit a [channel created](events.md#channel-created) event with the channel's ID.
-### Duplicate channel name
+### Name not valid
+
+This endpoint will respond with a status of `400 Bad Request` if the proposed `name` is not valid.
+
+### Channel name in use
This endpoint will respond with a status of `409 Conflict` if a channel with the requested name already exists.
diff --git a/src/channel/app.rs b/src/channel/app.rs
index 8359277..9a19b16 100644
--- a/src/channel/app.rs
+++ b/src/channel/app.rs
@@ -4,7 +4,7 @@ use sqlx::sqlite::SqlitePool;
use super::{
repo::{LoadError, Provider as _},
- Channel, Id,
+ validate, Channel, Id,
};
use crate::{
clock::DateTime,
@@ -25,6 +25,10 @@ impl<'a> Channels<'a> {
}
pub async fn create(&self, name: &Name, created_at: &DateTime) -> Result<Channel, CreateError> {
+ if !validate::name(name) {
+ return Err(CreateError::InvalidName(name.clone()));
+ }
+
let mut tx = self.db.begin().await?;
let created = tx.sequence().next(created_at).await?;
let channel = tx
@@ -149,6 +153,8 @@ impl<'a> Channels<'a> {
pub enum CreateError {
#[error("channel named {0} already exists")]
DuplicateName(Name),
+ #[error("invalid channel name: {0}")]
+ InvalidName(Name),
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error(transparent)]
diff --git a/src/channel/mod.rs b/src/channel/mod.rs
index eb8200b..d5ba828 100644
--- a/src/channel/mod.rs
+++ b/src/channel/mod.rs
@@ -5,5 +5,6 @@ mod id;
pub mod repo;
mod routes;
mod snapshot;
+mod validate;
pub use self::{event::Event, history::History, id::Id, routes::router, snapshot::Channel};
diff --git a/src/channel/routes/post.rs b/src/channel/routes/post.rs
index 810445c..2cf1cc0 100644
--- a/src/channel/routes/post.rs
+++ b/src/channel/routes/post.rs
@@ -54,6 +54,9 @@ impl IntoResponse for Error {
app::CreateError::DuplicateName(_) => {
(StatusCode::CONFLICT, error.to_string()).into_response()
}
+ app::CreateError::InvalidName(_) => {
+ (StatusCode::BAD_REQUEST, error.to_string()).into_response()
+ }
other => Internal::from(other).into_response(),
}
}
diff --git a/src/channel/routes/test.rs b/src/channel/routes/test.rs
index 10b1e8d..f5369fb 100644
--- a/src/channel/routes/test.rs
+++ b/src/channel/routes/test.rs
@@ -116,6 +116,30 @@ async fn conflicting_canonical_name() {
}
#[tokio::test]
+async fn invalid_name() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let creator = fixtures::identity::create(&app, &fixtures::now()).await;
+
+ // Call the endpoint
+
+ let name = fixtures::channel::propose_invalid_name();
+ let request = post::Request { name: name.clone() };
+ let post::Error(error) =
+ post::handler(State(app.clone()), creator, fixtures::now(), Json(request))
+ .await
+ .expect_err("invalid channel name should fail the request");
+
+ // Verify the structure of the response
+
+ assert!(matches!(
+ error,
+ app::CreateError::InvalidName(error_name) if name == error_name
+ ));
+}
+
+#[tokio::test]
async fn name_reusable_after_delete() {
// Set up the environment
diff --git a/src/channel/validate.rs b/src/channel/validate.rs
new file mode 100644
index 0000000..0c97293
--- /dev/null
+++ b/src/channel/validate.rs
@@ -0,0 +1,23 @@
+use unicode_segmentation::UnicodeSegmentation as _;
+
+use crate::name::Name;
+
+// Picked out of a hat. The power of two is not meaningful.
+const NAME_TOO_LONG: usize = 64;
+
+pub fn name(name: &Name) -> bool {
+ let display = name.display();
+
+ [
+ display.graphemes(true).count() < NAME_TOO_LONG,
+ display.chars().all(|ch| !ch.is_control()),
+ display.chars().next().is_some_and(|c| !c.is_whitespace()),
+ display.chars().last().is_some_and(|c| !c.is_whitespace()),
+ display
+ .chars()
+ .zip(display.chars().skip(1))
+ .all(|(a, b)| !(a.is_whitespace() && b.is_whitespace())),
+ ]
+ .into_iter()
+ .all(|value| value)
+}
diff --git a/src/test/fixtures/channel.rs b/src/test/fixtures/channel.rs
index 0c6480b..98048f2 100644
--- a/src/test/fixtures/channel.rs
+++ b/src/test/fixtures/channel.rs
@@ -1,6 +1,7 @@
use faker_rand::{
en_us::{addresses::CityName, names::FullName},
faker_impl_from_templates,
+ lorem::Paragraphs,
};
use rand;
@@ -23,6 +24,10 @@ pub fn propose() -> Name {
rand::random::<NameTemplate>().to_string().into()
}
+pub fn propose_invalid_name() -> Name {
+ rand::random::<Paragraphs>().to_string().into()
+}
+
struct NameTemplate(String);
faker_impl_from_templates! {
NameTemplate; "{} {}", CityName, FullName;