summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/events/routes/test.rs43
-rw-r--r--src/login/app.rs11
-rw-r--r--src/login/routes.rs17
-rw-r--r--src/login/routes/test/logout.rs30
-rw-r--r--src/repo/token.rs10
-rw-r--r--src/test/fixtures/identity.rs12
6 files changed, 91 insertions, 32 deletions
diff --git a/src/events/routes/test.rs b/src/events/routes/test.rs
index 0b62b5b..820192d 100644
--- a/src/events/routes/test.rs
+++ b/src/events/routes/test.rs
@@ -355,6 +355,7 @@ async fn terminates_on_token_expiry() {
let subscriber_creds = fixtures::login::create_with_password(&app).await;
let subscriber =
fixtures::identity::identity(&app, &subscriber_creds, &fixtures::ancient()).await;
+
let routes::Events(events) = routes::events(State(app.clone()), subscriber, None)
.await
.expect("subscribe never fails");
@@ -380,3 +381,45 @@ async fn terminates_on_token_expiry() {
.await
.is_none());
}
+
+#[tokio::test]
+async fn terminates_on_logout() {
+ // 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).await;
+
+ // Subscribe via the endpoint
+
+ let subscriber_creds = fixtures::login::create_with_password(&app).await;
+ let subscriber_token =
+ fixtures::identity::logged_in(&app, &subscriber_creds, &fixtures::now()).await;
+ let subscriber =
+ fixtures::identity::from_token(&app, &subscriber_token, &fixtures::now()).await;
+
+ let routes::Events(events) = routes::events(State(app.clone()), subscriber.clone(), None)
+ .await
+ .expect("subscribe never fails");
+
+ // Verify the resulting stream's behaviour
+
+ app.logins()
+ .logout(&subscriber.token)
+ .await
+ .expect("expiring tokens succeeds");
+
+ // These should not be delivered.
+ let messages = [
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ fixtures::message::send(&app, &sender, &channel, &fixtures::now()).await,
+ ];
+
+ assert!(events
+ .filter(|types::ResumableEvent(_, event)| future::ready(messages.contains(event)))
+ .next()
+ .immediately()
+ .await
+ .is_none());
+}
diff --git a/src/login/app.rs b/src/login/app.rs
index b8916a8..182c62c 100644
--- a/src/login/app.rs
+++ b/src/login/app.rs
@@ -120,16 +120,13 @@ impl<'a> Logins<'a> {
Ok(())
}
- pub async fn logout(&self, secret: &IdentitySecret) -> Result<(), ValidateError> {
+ pub async fn logout(&self, token: &token::Id) -> Result<(), ValidateError> {
let mut tx = self.db.begin().await?;
- let token = tx
- .tokens()
- .revoke(secret)
- .await
- .not_found(|| ValidateError::InvalidToken)?;
+ tx.tokens().revoke(token).await?;
tx.commit().await?;
- self.logins.broadcast(&types::TokenRevoked::from(token));
+ self.logins
+ .broadcast(&types::TokenRevoked::from(token.clone()));
Ok(())
}
diff --git a/src/login/routes.rs b/src/login/routes.rs
index 4664063..8d9e938 100644
--- a/src/login/routes.rs
+++ b/src/login/routes.rs
@@ -78,27 +78,32 @@ struct LogoutRequest {}
async fn on_logout(
State(app): State<App>,
+ RequestedAt(now): RequestedAt,
identity: IdentityToken,
// This forces the only valid request to be `{}`, and not the infinite
// variation allowed when there's no body extractor.
Json(LogoutRequest {}): Json<LogoutRequest>,
) -> Result<(IdentityToken, StatusCode), LogoutError> {
if let Some(secret) = identity.secret() {
- app.logins().logout(&secret).await.map_err(LogoutError)?;
+ let (token, _) = app.logins().validate(&secret, &now).await?;
+ app.logins().logout(&token).await?;
}
let identity = identity.clear();
Ok((identity, StatusCode::NO_CONTENT))
}
-#[derive(Debug)]
-struct LogoutError(app::ValidateError);
+#[derive(Debug, thiserror::Error)]
+#[error(transparent)]
+enum LogoutError {
+ ValidateError(#[from] app::ValidateError),
+ DatabaseError(#[from] sqlx::Error),
+}
impl IntoResponse for LogoutError {
fn into_response(self) -> Response {
- let Self(error) = self;
- match error {
- error @ app::ValidateError::InvalidToken => {
+ match self {
+ error @ Self::ValidateError(app::ValidateError::InvalidToken) => {
(StatusCode::UNAUTHORIZED, error.to_string()).into_response()
}
other => Internal::from(other).into_response(),
diff --git a/src/login/routes/test/logout.rs b/src/login/routes/test/logout.rs
index 05594be..20b0d55 100644
--- a/src/login/routes/test/logout.rs
+++ b/src/login/routes/test/logout.rs
@@ -22,6 +22,7 @@ async fn successful() {
let (response_identity, response_status) = routes::on_logout(
State(app.clone()),
+ fixtures::now(),
identity.clone(),
Json(routes::LogoutRequest {}),
)
@@ -57,10 +58,14 @@ async fn no_identity() {
// Call the endpoint
let identity = fixtures::identity::not_logged_in();
- let (identity, status) =
- routes::on_logout(State(app), identity, Json(routes::LogoutRequest {}))
- .await
- .expect("logged out with no token");
+ let (identity, status) = routes::on_logout(
+ State(app),
+ fixtures::now(),
+ identity,
+ Json(routes::LogoutRequest {}),
+ )
+ .await
+ .expect("logged out with no token");
// Verify the return value's basic structure
@@ -77,12 +82,19 @@ async fn invalid_token() {
// Call the endpoint
let identity = fixtures::identity::fictitious();
- let routes::LogoutError(error) =
- routes::on_logout(State(app), identity, Json(routes::LogoutRequest {}))
- .await
- .expect_err("logged out with an invalid token");
+ let error = routes::on_logout(
+ State(app),
+ fixtures::now(),
+ identity,
+ Json(routes::LogoutRequest {}),
+ )
+ .await
+ .expect_err("logged out with an invalid token");
// Verify the return value's basic structure
- assert!(matches!(error, app::ValidateError::InvalidToken));
+ assert!(matches!(
+ error,
+ routes::LogoutError::ValidateError(app::ValidateError::InvalidToken)
+ ));
}
diff --git a/src/repo/token.rs b/src/repo/token.rs
index 5f39e1d..d96c094 100644
--- a/src/repo/token.rs
+++ b/src/repo/token.rs
@@ -48,20 +48,20 @@ impl<'c> Tokens<'c> {
}
// Revoke a token by its secret.
- pub async fn revoke(&mut self, secret: &IdentitySecret) -> Result<Id, sqlx::Error> {
- let token = sqlx::query_scalar!(
+ pub async fn revoke(&mut self, token: &Id) -> Result<(), sqlx::Error> {
+ sqlx::query_scalar!(
r#"
delete
from token
- where secret = $1
+ where id = $1
returning id as "id: Id"
"#,
- secret,
+ token,
)
.fetch_one(&mut *self.0)
.await?;
- Ok(token)
+ Ok(())
}
// Expire and delete all tokens that haven't been used more recently than
diff --git a/src/test/fixtures/identity.rs b/src/test/fixtures/identity.rs
index bdd7881..633fb8a 100644
--- a/src/test/fixtures/identity.rs
+++ b/src/test/fixtures/identity.rs
@@ -22,11 +22,8 @@ pub async fn logged_in(app: &App, login: &(String, Password), now: &RequestedAt)
IdentityToken::new().set(token)
}
-pub async fn identity(app: &App, login: &(String, Password), issued_at: &RequestedAt) -> Identity {
- let secret = logged_in(app, login, issued_at)
- .await
- .secret()
- .expect("successful login generates a secret");
+pub async fn from_token(app: &App, token: &IdentityToken, issued_at: &RequestedAt) -> Identity {
+ let secret = token.secret().expect("identity token has a secret");
let (token, login) = app
.logins()
.validate(&secret, issued_at)
@@ -36,6 +33,11 @@ pub async fn identity(app: &App, login: &(String, Password), issued_at: &Request
Identity { token, login }
}
+pub async fn identity(app: &App, login: &(String, Password), issued_at: &RequestedAt) -> Identity {
+ let secret = logged_in(app, login, issued_at).await;
+ from_token(app, &secret, issued_at).await
+}
+
pub fn secret(identity: &IdentityToken) -> IdentitySecret {
identity.secret().expect("identity contained a secret")
}