171 lines
5.0 KiB
Rust
171 lines
5.0 KiB
Rust
/*
|
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
|
* Source-Available Code / Dual-Licensed.
|
|
*
|
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
|
* Commercial or production use requires a valid paid license.
|
|
* See LICENSE file in repository root for details.
|
|
*/
|
|
|
|
use super::*;
|
|
use crate::{
|
|
database::Database,
|
|
state::AppState,
|
|
storage::Storage,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
async fn logout_test_state() -> SharedState {
|
|
let db = Database::connect("sqlite::memory:", 1)
|
|
.await
|
|
.expect("test database");
|
|
crate::run_migrations(&db).await.expect("test migrations");
|
|
Arc::new(AppState::new(
|
|
db,
|
|
"test".into(),
|
|
Storage::Local {
|
|
root: std::env::temp_dir().join("rustpad-logout-tests"),
|
|
},
|
|
1_000_000,
|
|
true,
|
|
1_000_000,
|
|
0,
|
|
None,
|
|
None,
|
|
true,
|
|
false,
|
|
false,
|
|
"error".into(),
|
|
7,
|
|
7,
|
|
7,
|
|
None,
|
|
))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn logout_only_revokes_current_browser_session_and_password_access() {
|
|
let state = logout_test_state().await;
|
|
sqlx::query(
|
|
"INSERT INTO users (nickname, nickname_key, email, email_key, password_hash) VALUES (?, ?, ?, ?, ?)",
|
|
)
|
|
.bind("Logout Test")
|
|
.bind("logout test")
|
|
.bind("logout@example.test")
|
|
.bind("logout@example.test")
|
|
.bind("password-hash")
|
|
.execute(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
|
|
let expires_at = (Utc::now() + Duration::days(7)).to_rfc3339();
|
|
for session in ["current-session", "other-device-session"] {
|
|
sqlx::query("INSERT INTO user_sessions (token, user_id, expires_at) VALUES (?, 1, ?)")
|
|
.bind(session)
|
|
.bind(&expires_at)
|
|
.execute(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let current_access_token = "a".repeat(64);
|
|
let other_access_token = "b".repeat(64);
|
|
for (token, slug) in [
|
|
(current_access_token.as_str(), "current-pad"),
|
|
(other_access_token.as_str(), "other-pad"),
|
|
] {
|
|
sqlx::query(
|
|
"INSERT INTO resource_access_tokens (token_hash, resource_kind, resource_slug, expires_at) VALUES (?, 'pad', ?, ?)",
|
|
)
|
|
.bind(hash_token(token))
|
|
.bind(slug)
|
|
.bind(&expires_at)
|
|
.execute(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let access_cookie = crate::security::resource_cookie(
|
|
"pad",
|
|
"current-pad",
|
|
¤t_access_token,
|
|
7,
|
|
);
|
|
let access_cookie_name = access_cookie
|
|
.to_str()
|
|
.unwrap()
|
|
.split_once('=')
|
|
.unwrap()
|
|
.0
|
|
.to_string();
|
|
let share_cookie_name = "__Host-rustpad_share_0123456789abcdef01234567";
|
|
let mut headers = HeaderMap::new();
|
|
headers.insert(
|
|
header::COOKIE,
|
|
axum::http::HeaderValue::from_str(&format!(
|
|
"{}=current-session; {}={}; {}=share-token",
|
|
crate::security::SESSION_COOKIE,
|
|
access_cookie_name,
|
|
current_access_token,
|
|
share_cookie_name,
|
|
))
|
|
.unwrap(),
|
|
);
|
|
|
|
let response = match logout(State(state.clone()), headers).await {
|
|
Ok(response) => response,
|
|
Err(error) => panic!("logout failed: {}", error.message),
|
|
};
|
|
|
|
let current_session_count: i64 =
|
|
sqlx::query_scalar("SELECT COUNT(*) FROM user_sessions WHERE token = ?")
|
|
.bind("current-session")
|
|
.fetch_one(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
let other_session_count: i64 =
|
|
sqlx::query_scalar("SELECT COUNT(*) FROM user_sessions WHERE token = ?")
|
|
.bind("other-device-session")
|
|
.fetch_one(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
let current_access_count: i64 = sqlx::query_scalar(
|
|
"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ?",
|
|
)
|
|
.bind(hash_token(¤t_access_token))
|
|
.fetch_one(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
let other_access_count: i64 = sqlx::query_scalar(
|
|
"SELECT COUNT(*) FROM resource_access_tokens WHERE token_hash = ?",
|
|
)
|
|
.bind(hash_token(&other_access_token))
|
|
.fetch_one(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
let user_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE id = 1")
|
|
.fetch_one(state.db.pool())
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(current_session_count, 0);
|
|
assert_eq!(other_session_count, 1);
|
|
assert_eq!(current_access_count, 0);
|
|
assert_eq!(other_access_count, 1);
|
|
assert_eq!(user_count, 1);
|
|
|
|
let set_cookies = response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.map(|value| value.to_str().unwrap())
|
|
.collect::<Vec<_>>();
|
|
assert!(set_cookies.iter().any(|value| {
|
|
value.starts_with(&format!("{}=;", crate::security::SESSION_COOKIE))
|
|
}));
|
|
assert!(set_cookies.iter().any(|value| {
|
|
value.starts_with(&format!("{access_cookie_name}=;"))
|
|
}));
|
|
assert!(!set_cookies.iter().any(|value| value.starts_with(share_cookie_name)));
|
|
}
|