fixes and functions

This commit is contained in:
Mateusz Gruszczyński
2026-08-05 23:52:41 +02:00
parent 4643de4309
commit a7329f5535
13 changed files with 330 additions and 15 deletions
+170
View File
@@ -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",
&current_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(&current_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)));
}