pub struct LogoutService {}
#[cfg(feature = "sessions")]
pub struct SessionLogoutService<R> {
revoker: crate::sessions::services::SessionRevoker<R>,
}
impl LogoutService {
pub fn new() -> Self {
Self {}
}
pub fn logout(&self) {}
}
#[cfg(feature = "sessions")]
impl<R> SessionLogoutService<R>
where
R: crate::sessions::repository::SessionRepository,
{
pub fn new(repository: R) -> Self {
Self {
revoker: crate::sessions::services::SessionRevoker::new(repository),
}
}
pub async fn logout(
&self,
request: crate::sessions::logout::LogoutRequest,
) -> crate::sessions::errors::Result<crate::sessions::logout::LogoutOutcome> {
self.revoker.revoke_session(request).await
}
}
impl Default for LogoutService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[cfg(feature = "sessions")]
mod tests {
use super::{LogoutService, SessionLogoutService};
use crate::sessions::logout::{LogoutOutcome, LogoutRequest, LogoutScope};
use crate::sessions::repository::{
CreateSession, RepositoryResult, RevokeSessionScope, RotateRefreshToken,
RotateRefreshTokenOutcome, SessionRepository,
};
use crate::sessions::session::{
SessionFamilyId, SessionFamilyRecord, SessionId, SessionLookup, SessionRecord,
SessionRefreshRecord, SessionTouch,
};
use crate::sessions::tokens::RefreshTokenHashRef;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct DummySessionRepository {
revoked_sessions: Arc<Mutex<Vec<(SessionId, RevokeSessionScope)>>>,
revoked_families: Arc<Mutex<Vec<SessionFamilyId>>>,
}
impl SessionRepository for DummySessionRepository {
async fn create_session(&self, _input: CreateSession) -> RepositoryResult<()> {
Ok(())
}
async fn find_session_by_refresh_token_hash<'a>(
&'a self,
_refresh_token_hash: RefreshTokenHashRef<'a>,
) -> RepositoryResult<Option<SessionLookup>> {
Ok(None)
}
async fn find_session(
&self,
_session_id: SessionId,
) -> RepositoryResult<Option<SessionRecord>> {
Ok(None)
}
async fn find_family(
&self,
_family_id: SessionFamilyId,
) -> RepositoryResult<Option<SessionFamilyRecord>> {
Ok(None)
}
async fn find_refresh_record(
&self,
_session_id: SessionId,
) -> RepositoryResult<Option<SessionRefreshRecord>> {
Ok(None)
}
async fn try_acquire_renewal_lease(
&self,
_session_id: SessionId,
_lease: crate::sessions::lease::RenewalLease,
) -> RepositoryResult<crate::sessions::lease::LeaseAcquisition> {
Ok(crate::sessions::lease::LeaseAcquisition::Unavailable)
}
async fn rotate_refresh_token(
&self,
_input: RotateRefreshToken,
) -> RepositoryResult<RotateRefreshTokenOutcome> {
Ok(RotateRefreshTokenOutcome::LeaseUnavailable)
}
async fn revoke_session(
&self,
session_id: SessionId,
scope: RevokeSessionScope,
) -> RepositoryResult<()> {
match self.revoked_sessions.lock() {
Ok(mut revoked_sessions) => {
revoked_sessions.push((session_id, scope));
}
Err(error) => {
panic!("session revocation lock should not be poisoned: {}", error);
}
}
Ok(())
}
async fn revoke_family(&self, family_id: SessionFamilyId) -> RepositoryResult<()> {
match self.revoked_families.lock() {
Ok(mut revoked_families) => {
revoked_families.push(family_id);
}
Err(error) => {
panic!("family revocation lock should not be poisoned: {}", error);
}
}
Ok(())
}
async fn touch_session(&self, _touch: SessionTouch) -> RepositoryResult<()> {
Ok(())
}
}
#[test]
fn classic_logout_service_is_constructible() {
let service = LogoutService::new();
service.logout();
}
#[tokio::test]
async fn session_logout_service_revokes_current_session() {
let repository = DummySessionRepository::default();
let service = SessionLogoutService::new(repository.clone());
let session_id = SessionId::new();
let family_id = SessionFamilyId::new();
let outcome = match service
.logout(LogoutRequest::new(
session_id,
family_id,
LogoutScope::CurrentSession,
))
.await
{
Ok(outcome) => outcome,
Err(error) => panic!("session logout should succeed: {}", error),
};
assert_eq!(outcome, LogoutOutcome::CurrentSessionRevoked);
let revoked_sessions = match repository.revoked_sessions.lock() {
Ok(revoked_sessions) => revoked_sessions,
Err(error) => panic!("session revocation lock should not be poisoned: {}", error),
};
assert_eq!(
revoked_sessions.as_slice(),
&[(session_id, RevokeSessionScope::CurrentSession)]
);
}
#[tokio::test]
async fn session_logout_service_revokes_session_family() {
let repository = DummySessionRepository::default();
let service = SessionLogoutService::new(repository.clone());
let session_id = SessionId::new();
let family_id = SessionFamilyId::new();
let outcome = match service
.logout(LogoutRequest::new(
session_id,
family_id,
LogoutScope::SessionFamily,
))
.await
{
Ok(outcome) => outcome,
Err(error) => panic!("session family logout should succeed: {}", error),
};
assert_eq!(outcome, LogoutOutcome::SessionFamilyRevoked);
let revoked_families = match repository.revoked_families.lock() {
Ok(revoked_families) => revoked_families,
Err(error) => panic!("family revocation lock should not be poisoned: {}", error),
};
assert_eq!(revoked_families.as_slice(), &[family_id]);
}
}