summaryrefslogtreecommitdiff
path: root/src/invite
diff options
context:
space:
mode:
Diffstat (limited to 'src/invite')
-rw-r--r--src/invite/app.rs16
-rw-r--r--src/invite/mod.rs2
-rw-r--r--src/invite/routes/invite/mod.rs2
-rw-r--r--src/invite/routes/invite/post.rs3
-rw-r--r--src/invite/routes/invite/test/get.rs65
-rw-r--r--src/invite/routes/invite/test/mod.rs2
-rw-r--r--src/invite/routes/invite/test/post.rs208
-rw-r--r--src/invite/routes/mod.rs2
-rw-r--r--src/invite/routes/post.rs2
-rw-r--r--src/invite/routes/test.rs28
10 files changed, 319 insertions, 11 deletions
diff --git a/src/invite/app.rs b/src/invite/app.rs
index 64ba753..176075f 100644
--- a/src/invite/app.rs
+++ b/src/invite/app.rs
@@ -5,7 +5,7 @@ use super::{repo::Provider as _, Id, Invite, Summary};
use crate::{
clock::DateTime,
db::{Duplicate as _, NotFound as _},
- event::repo::Provider as _,
+ event::{repo::Provider as _, Broadcaster, Event},
login::{repo::Provider as _, Login, Password},
name::Name,
token::{repo::Provider as _, Secret},
@@ -13,18 +13,15 @@ use crate::{
pub struct Invites<'a> {
db: &'a SqlitePool,
+ events: &'a Broadcaster,
}
impl<'a> Invites<'a> {
- pub const fn new(db: &'a SqlitePool) -> Self {
- Self { db }
+ pub const fn new(db: &'a SqlitePool, events: &'a Broadcaster) -> Self {
+ Self { db, events }
}
- pub async fn create(
- &self,
- issuer: &Login,
- issued_at: &DateTime,
- ) -> Result<Invite, sqlx::Error> {
+ pub async fn issue(&self, issuer: &Login, issued_at: &DateTime) -> Result<Invite, sqlx::Error> {
let mut tx = self.db.begin().await?;
let invite = tx.invites().create(issuer, issued_at).await?;
tx.commit().await?;
@@ -73,6 +70,9 @@ impl<'a> Invites<'a> {
let secret = tx.tokens().issue(&login, accepted_at).await?;
tx.commit().await?;
+ self.events
+ .broadcast(login.events().map(Event::from).collect::<Vec<_>>());
+
Ok((login.as_created(), secret))
}
diff --git a/src/invite/mod.rs b/src/invite/mod.rs
index d59fb9c..53ca984 100644
--- a/src/invite/mod.rs
+++ b/src/invite/mod.rs
@@ -14,7 +14,7 @@ pub struct Invite {
pub issued_at: DateTime,
}
-#[derive(serde::Serialize)]
+#[derive(Debug, serde::Serialize)]
pub struct Summary {
pub id: Id,
pub issuer: nfc::String,
diff --git a/src/invite/routes/invite/mod.rs b/src/invite/routes/invite/mod.rs
index 04593fd..c22029a 100644
--- a/src/invite/routes/invite/mod.rs
+++ b/src/invite/routes/invite/mod.rs
@@ -1,4 +1,6 @@
pub mod get;
pub mod post;
+#[cfg(test)]
+pub mod test;
type PathInfo = crate::invite::Id;
diff --git a/src/invite/routes/invite/post.rs b/src/invite/routes/invite/post.rs
index 3ca4e6b..0dd8dba 100644
--- a/src/invite/routes/invite/post.rs
+++ b/src/invite/routes/invite/post.rs
@@ -36,7 +36,8 @@ pub struct Request {
pub password: Password,
}
-pub struct Error(app::AcceptError);
+#[derive(Debug)]
+pub struct Error(pub app::AcceptError);
impl IntoResponse for Error {
fn into_response(self) -> Response {
diff --git a/src/invite/routes/invite/test/get.rs b/src/invite/routes/invite/test/get.rs
new file mode 100644
index 0000000..c6780ed
--- /dev/null
+++ b/src/invite/routes/invite/test/get.rs
@@ -0,0 +1,65 @@
+use axum::extract::{Json, Path, State};
+
+use crate::{invite::routes::invite::get, test::fixtures};
+
+#[tokio::test]
+async fn valid_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::now()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await;
+
+ // Call endpoint
+
+ let Json(response) = get::handler(State(app), Path(invite.id))
+ .await
+ .expect("get for an existing invite succeeds");
+
+ // Verify response
+
+ assert_eq!(issuer.name.display(), &response.issuer);
+ assert_eq!(invite.issued_at, response.issued_at);
+}
+
+#[tokio::test]
+async fn nonexistent_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+
+ // Call endpoint
+
+ let invite = fixtures::invite::fictitious();
+ let error = get::handler(State(app), Path(invite.clone()))
+ .await
+ .expect_err("get for a nonexistent invite fails");
+
+ // Verify response
+
+ assert!(matches!(error, get::Error::NotFound(error_id) if invite == error_id));
+}
+
+#[tokio::test]
+async fn expired_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::ancient()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await;
+
+ app.invites()
+ .expire(&fixtures::now())
+ .await
+ .expect("expiring invites never fails");
+
+ // Call endpoint
+
+ let error = get::handler(State(app), Path(invite.id.clone()))
+ .await
+ .expect_err("get for an expired invite fails");
+
+ // Verify response
+
+ assert!(matches!(error, get::Error::NotFound(error_id) if invite.id == error_id));
+}
diff --git a/src/invite/routes/invite/test/mod.rs b/src/invite/routes/invite/test/mod.rs
new file mode 100644
index 0000000..d6c1f06
--- /dev/null
+++ b/src/invite/routes/invite/test/mod.rs
@@ -0,0 +1,2 @@
+mod get;
+mod post;
diff --git a/src/invite/routes/invite/test/post.rs b/src/invite/routes/invite/test/post.rs
new file mode 100644
index 0000000..65ab61e
--- /dev/null
+++ b/src/invite/routes/invite/test/post.rs
@@ -0,0 +1,208 @@
+use axum::extract::{Json, Path, State};
+
+use crate::{
+ invite::{app::AcceptError, routes::invite::post},
+ name::Name,
+ test::fixtures,
+};
+
+#[tokio::test]
+async fn valid_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::now()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::now()).await;
+
+ // Call the endpoint
+
+ let (name, password) = fixtures::login::propose();
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request {
+ name: name.clone(),
+ password: password.clone(),
+ };
+ let (identity, Json(response)) = post::handler(
+ State(app.clone()),
+ fixtures::now(),
+ identity,
+ Path(invite.id),
+ Json(request),
+ )
+ .await
+ .expect("accepting a valid invite succeeds");
+
+ // Verify the response
+
+ assert!(identity.secret().is_some());
+ assert_eq!(name, response.name);
+
+ // Verify that the issued token is valid
+
+ let secret = identity
+ .secret()
+ .expect("newly-issued identity has a token secret");
+ let (_, login) = app
+ .tokens()
+ .validate(&secret, &fixtures::now())
+ .await
+ .expect("newly-issued identity cookie is valid");
+ assert_eq!(response, login);
+
+ // Verify that the given credentials can log in
+
+ let (login, _) = app
+ .tokens()
+ .login(&name, &password, &fixtures::now())
+ .await
+ .expect("credentials given on signup are valid");
+ assert_eq!(response, login);
+}
+
+#[tokio::test]
+async fn nonexistent_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let invite = fixtures::invite::fictitious();
+
+ // Call the endpoint
+
+ let (name, password) = fixtures::login::propose();
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request {
+ name: name.clone(),
+ password: password.clone(),
+ };
+ let post::Error(error) = post::handler(
+ State(app.clone()),
+ fixtures::now(),
+ identity,
+ Path(invite.clone()),
+ Json(request),
+ )
+ .await
+ .expect_err("accepting a nonexistent invite fails");
+
+ // Verify the response
+
+ assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite));
+}
+
+#[tokio::test]
+async fn expired_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::ancient()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await;
+
+ app.invites()
+ .expire(&fixtures::now())
+ .await
+ .expect("expiring invites never fails");
+
+ // Call the endpoint
+
+ let (name, password) = fixtures::login::propose();
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request {
+ name: name.clone(),
+ password: password.clone(),
+ };
+ let post::Error(error) = post::handler(
+ State(app.clone()),
+ fixtures::now(),
+ identity,
+ Path(invite.id.clone()),
+ Json(request),
+ )
+ .await
+ .expect_err("accepting a nonexistent invite fails");
+
+ // Verify the response
+
+ assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite.id));
+}
+
+#[tokio::test]
+async fn accepted_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::ancient()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await;
+
+ let (name, password) = fixtures::login::propose();
+ app.invites()
+ .accept(&invite.id, &name, &password, &fixtures::now())
+ .await
+ .expect("accepting a valid invite succeeds");
+
+ // Call the endpoint
+
+ let (name, password) = fixtures::login::propose();
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request {
+ name: name.clone(),
+ password: password.clone(),
+ };
+ let post::Error(error) = post::handler(
+ State(app.clone()),
+ fixtures::now(),
+ identity,
+ Path(invite.id.clone()),
+ Json(request),
+ )
+ .await
+ .expect_err("accepting a nonexistent invite fails");
+
+ // Verify the response
+
+ assert!(matches!(error, AcceptError::NotFound(error_id) if error_id == invite.id));
+}
+
+#[tokio::test]
+async fn conflicting_name() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::login::create(&app, &fixtures::ancient()).await;
+ let invite = fixtures::invite::issue(&app, &issuer, &fixtures::ancient()).await;
+
+ let existing_name = Name::from("rijksmuseum");
+ app.logins()
+ .create(
+ &existing_name,
+ &fixtures::login::propose_password(),
+ &fixtures::now(),
+ )
+ .await
+ .expect("creating a login in an empty environment succeeds");
+
+ // Call the endpoint
+
+ let conflicting_name = Name::from("r\u{0133}ksmuseum");
+ let password = fixtures::login::propose_password();
+
+ let identity = fixtures::cookie::not_logged_in();
+ let request = post::Request {
+ name: conflicting_name.clone(),
+ password: password.clone(),
+ };
+ let post::Error(error) = post::handler(
+ State(app.clone()),
+ fixtures::now(),
+ identity,
+ Path(invite.id.clone()),
+ Json(request),
+ )
+ .await
+ .expect_err("accepting a nonexistent invite fails");
+
+ // Verify the response
+
+ assert!(
+ matches!(error, AcceptError::DuplicateLogin(error_name) if error_name == conflicting_name)
+ );
+}
diff --git a/src/invite/routes/mod.rs b/src/invite/routes/mod.rs
index dae20ba..2f7375c 100644
--- a/src/invite/routes/mod.rs
+++ b/src/invite/routes/mod.rs
@@ -7,6 +7,8 @@ use crate::app::App;
mod invite;
mod post;
+#[cfg(test)]
+mod test;
pub fn router() -> Router<App> {
Router::new()
diff --git a/src/invite/routes/post.rs b/src/invite/routes/post.rs
index eb7d706..898081e 100644
--- a/src/invite/routes/post.rs
+++ b/src/invite/routes/post.rs
@@ -10,7 +10,7 @@ pub async fn handler(
identity: Identity,
_: Json<Request>,
) -> Result<Json<Invite>, Internal> {
- let invite = app.invites().create(&identity.login, &issued_at).await?;
+ let invite = app.invites().issue(&identity.login, &issued_at).await?;
Ok(Json(invite))
}
diff --git a/src/invite/routes/test.rs b/src/invite/routes/test.rs
new file mode 100644
index 0000000..4d99660
--- /dev/null
+++ b/src/invite/routes/test.rs
@@ -0,0 +1,28 @@
+use axum::extract::{Json, State};
+
+use super::post;
+use crate::test::fixtures;
+
+#[tokio::test]
+async fn create_invite() {
+ // Set up the environment
+
+ let app = fixtures::scratch_app().await;
+ let issuer = fixtures::identity::create(&app, &fixtures::now()).await;
+ let issued_at = fixtures::now();
+
+ // Call the endpoint
+
+ let Json(invite) = post::handler(
+ State(app),
+ issued_at.clone(),
+ issuer.clone(),
+ Json(post::Request {}),
+ )
+ .await
+ .expect("creating an invite always succeeds");
+
+ // Verify the response
+ assert_eq!(issuer.login.id, invite.issuer);
+ assert_eq!(&*issued_at, &invite.issued_at);
+}