summaryrefslogtreecommitdiff
path: root/src/boot/handlers
diff options
context:
space:
mode:
authorojacobson <ojacobson@noreply.codeberg.org>2025-06-21 04:22:52 +0200
committerojacobson <ojacobson@noreply.codeberg.org>2025-06-21 04:22:52 +0200
commitcd1dc0dab4b46bc5712070812192d5ce34071470 (patch)
treec94f5a42f7e734b81892c1289a1d2b566706ba7c /src/boot/handlers
parentd84ba5cd09b713fac2f193d5c05af7415ea6742d (diff)
parent4e3d5ccac99b24934c972e088cd7eb02bb95df06 (diff)
Reorganize and consolidate HTTP routes.
HTTP routes are now defined in a single, unified module, pulling them out of the topical modules they were formerly part of. This is intended to improve the navigability of the codebase. Previously, finding the handler corresponding to a specific endpoint required prior familiarity, though in practice you could usually guess from topic area. Now, all routes are defined in one place; if you know the path, you can read down the list to find the handler. Handlers themselves live with the domain they are most appropriately "part of," generally (in this version, universally) in a `handlers` submodule. The handlers themselves have been flattened down; rather than representing a path and a method, they now represent a named operation (which is suspiciously similar to the path in most cases). This means that we no longer have constructs like `crate::ui::routes::ch::channel` - it's now `crate::ui::handlers::channel` instead. ## Disclaimer I Solemnly Swear I Didn't Change Any Handlers. ## Prior art I've inadvertently reinvented Django's `urls.py` convention, and I've opted to lean into that. Merges flatter-routes-reorg into main.
Diffstat (limited to 'src/boot/handlers')
-rw-r--r--src/boot/handlers/boot/mod.rs30
-rw-r--r--src/boot/handlers/boot/test.rs133
-rw-r--r--src/boot/handlers/mod.rs3
3 files changed, 166 insertions, 0 deletions
diff --git a/src/boot/handlers/boot/mod.rs b/src/boot/handlers/boot/mod.rs
new file mode 100644
index 0000000..010f57b
--- /dev/null
+++ b/src/boot/handlers/boot/mod.rs
@@ -0,0 +1,30 @@
+use axum::{
+ extract::{Json, State},
+ response::{self, IntoResponse},
+};
+
+use crate::{app::App, boot::Snapshot, error::Internal, token::extract::Identity, user::User};
+
+#[cfg(test)]
+mod test;
+
+pub async fn handler(State(app): State<App>, identity: Identity) -> Result<Response, Internal> {
+ let snapshot = app.boot().snapshot().await?;
+ Ok(Response {
+ user: identity.user,
+ snapshot,
+ })
+}
+
+#[derive(serde::Serialize)]
+pub struct Response {
+ pub user: User,
+ #[serde(flatten)]
+ pub snapshot: Snapshot,
+}
+
+impl IntoResponse for Response {
+ fn into_response(self) -> response::Response {
+ Json(self).into_response()
+ }
+}
diff --git a/src/boot/handlers/boot/test.rs b/src/boot/handlers/boot/test.rs
new file mode 100644
index 0000000..0a7622b
--- /dev/null
+++ b/src/boot/handlers/boot/test.rs
@@ -0,0 +1,133 @@
+use axum::extract::State;
+
+use crate::test::fixtures;
+
+#[tokio::test]
+async fn returns_identity() {
+ let app = fixtures::scratch_app().await;
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer.clone())
+ .await
+ .expect("boot always succeeds");
+
+ assert_eq!(viewer.user, response.user);
+}
+
+#[tokio::test]
+async fn includes_logins() {
+ let app = fixtures::scratch_app().await;
+ let spectator = fixtures::user::create(&app, &fixtures::now()).await;
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(response.snapshot.users.contains(&spectator));
+}
+
+#[tokio::test]
+async fn includes_channels() {
+ let app = fixtures::scratch_app().await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(response.snapshot.channels.contains(&channel));
+}
+
+#[tokio::test]
+async fn includes_messages() {
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::user::create(&app, &fixtures::now()).await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+ let message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(response.snapshot.messages.contains(&message));
+}
+
+#[tokio::test]
+async fn excludes_expired_messages() {
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::user::create(&app, &fixtures::ancient()).await;
+ let channel = fixtures::channel::create(&app, &fixtures::ancient()).await;
+ let expired_message =
+ fixtures::message::send(&app, &channel, &sender, &fixtures::ancient()).await;
+
+ app.messages()
+ .expire(&fixtures::now())
+ .await
+ .expect("expiry never fails");
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(!response.snapshot.messages.contains(&expired_message));
+}
+
+#[tokio::test]
+async fn excludes_deleted_messages() {
+ let app = fixtures::scratch_app().await;
+ let sender = fixtures::user::create(&app, &fixtures::now()).await;
+ let channel = fixtures::channel::create(&app, &fixtures::now()).await;
+ let deleted_message = fixtures::message::send(&app, &channel, &sender, &fixtures::now()).await;
+
+ app.messages()
+ .delete(&sender, &deleted_message.id, &fixtures::now())
+ .await
+ .expect("deleting valid message succeeds");
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(!response.snapshot.messages.contains(&deleted_message));
+}
+
+#[tokio::test]
+async fn excludes_expired_channels() {
+ let app = fixtures::scratch_app().await;
+ let expired_channel = fixtures::channel::create(&app, &fixtures::ancient()).await;
+
+ app.channels()
+ .expire(&fixtures::now())
+ .await
+ .expect("expiry never fails");
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(!response.snapshot.channels.contains(&expired_channel));
+}
+
+#[tokio::test]
+async fn excludes_deleted_channels() {
+ let app = fixtures::scratch_app().await;
+ let deleted_channel = fixtures::channel::create(&app, &fixtures::now()).await;
+
+ app.channels()
+ .delete(&deleted_channel.id, &fixtures::now())
+ .await
+ .expect("deleting a valid channel succeeds");
+
+ let viewer = fixtures::identity::fictitious();
+ let response = super::handler(State(app), viewer)
+ .await
+ .expect("boot always succeeds");
+
+ assert!(!response.snapshot.channels.contains(&deleted_channel));
+}
diff --git a/src/boot/handlers/mod.rs b/src/boot/handlers/mod.rs
new file mode 100644
index 0000000..194c4a9
--- /dev/null
+++ b/src/boot/handlers/mod.rs
@@ -0,0 +1,3 @@
+mod boot;
+
+pub use boot::handler as boot;