fixes and functions
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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)));
|
||||
}
|
||||
Reference in New Issue
Block a user