[fix] VerNum

This commit is contained in:
Alex Emmet 2026-09-10 17:53:09 +02:00
commit 0827882bb3
No known key found for this signature in database
30 changed files with 1651 additions and 240 deletions

View file

@ -46,37 +46,7 @@ pub fn try_add_user_with_credential_origin(
credential_origin: CredentialOrigin,
) -> Result<(), crate::storage_error::StorageError> {
db::with_immediate_transaction(|tx| {
tx.execute(
r#"
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(user_id) DO UPDATE SET
username = excluded.username,
public_key = excluded.public_key,
private_key_hash = excluded.private_key_hash,
reset_token = excluded.reset_token,
display_name = excluded.display_name
"#,
params![
user.user_id,
user.username,
user.public_key,
user.private_key_hash,
user.reset_token,
user.created_at,
user.display_name,
],
)?;
for (app_id, app_secret) in &user.trusted_apps {
tx.execute(
r#"
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
VALUES (?1, ?2, ?3)
"#,
params![user.user_id, app_id, app_secret],
)?;
}
persist_user_profile(tx, &user)?;
tx.execute(
r#"INSERT INTO user_residency (user_id, username, lifecycle_state, data_state, credential_origin, updated_at)
VALUES (?1, ?2, 'managed', COALESCE((SELECT data_state FROM user_residency WHERE user_id = ?1), 'present'), ?3, ?4)
@ -88,7 +58,46 @@ pub fn try_add_user_with_credential_origin(
}
pub fn update_user(user: UserProfile) -> Result<(), crate::storage_error::StorageError> {
try_add_user(user)
db::with_immediate_transaction(|tx| persist_user_profile(tx, &user))
}
/// Persist mutable profile data without modifying lifecycle-managed residency.
fn persist_user_profile(
tx: &rusqlite::Transaction<'_>,
user: &UserProfile,
) -> Result<(), crate::storage_error::StorageError> {
tx.execute(
r#"
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT(user_id) DO UPDATE SET
username = excluded.username,
public_key = excluded.public_key,
private_key_hash = excluded.private_key_hash,
reset_token = excluded.reset_token,
display_name = excluded.display_name
"#,
params![
user.user_id,
user.username,
user.public_key,
user.private_key_hash,
user.reset_token,
user.created_at,
user.display_name,
],
)?;
for (app_id, app_secret) in &user.trusted_apps {
tx.execute(
r#"
INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret)
VALUES (?1, ?2, ?3)
"#,
params![user.user_id, app_id, app_secret],
)?;
}
Ok(())
}
pub fn get_user_by_username(
@ -659,9 +668,12 @@ pub fn load_users_sync() -> std::io::Result<()> {
}
#[cfg(test)]
mod purge_tests {
use super::{PurgeStage, remaining_purge_stages, run_purge_stages};
mod tests {
use super::{
PurgeStage, UserProfile, persist_user_profile, remaining_purge_stages, run_purge_stages,
};
use crate::users::pending_operations::PendingUserOperationPhase;
use rusqlite::{Connection, params};
#[test]
fn prepared_purge_runs_every_stage_before_completion() {
@ -725,6 +737,82 @@ mod purge_tests {
assert!(!data_empty);
}
}
#[test]
fn profile_updates_preserve_external_residency_attributes() {
let mut connection = Connection::open_in_memory().unwrap();
connection
.execute_batch(
r#"
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
public_key TEXT NOT NULL,
private_key_hash TEXT,
reset_token TEXT,
created_at INTEGER NOT NULL,
display_name TEXT
);
CREATE TABLE trusted_apps (
user_id INTEGER NOT NULL,
app_id TEXT NOT NULL,
app_secret TEXT NOT NULL,
PRIMARY KEY (user_id, app_id)
);
CREATE TABLE user_residency (
user_id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
lifecycle_state TEXT NOT NULL,
data_state TEXT NOT NULL,
credential_origin TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
INSERT INTO users VALUES (1, 'alice', 'old-key', NULL, NULL, 1, NULL);
INSERT INTO user_residency VALUES (1, 'alice', 'released', 'empty', 'external', 42);
"#,
)
.unwrap();
let mut user = UserProfile::new_with_created_at(
1,
"alice".into(),
Some("Alice".into()),
"new-key".into(),
None,
None,
1,
);
user.trusted_apps.insert("app".into(), "secret".into());
let transaction = connection.transaction().unwrap();
persist_user_profile(&transaction, &user).unwrap();
transaction.commit().unwrap();
let residency: (String, String, String, String, i64) = connection
.query_row(
"SELECT username, lifecycle_state, data_state, credential_origin, updated_at FROM user_residency WHERE user_id = ?1",
params![1],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)),
)
.unwrap();
assert_eq!(
residency,
(
"alice".into(),
"released".into(),
"empty".into(),
"external".into(),
42,
)
);
let trusted_app_count: i64 = connection
.query_row(
"SELECT COUNT(*) FROM trusted_apps WHERE user_id = ?1",
params![1],
|row| row.get(0),
)
.unwrap();
assert_eq!(trusted_app_count, 1);
}
}
pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) {

View file

@ -1,4 +1,4 @@
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use r2d2::ManageConnection;
use rusqlite::{Connection, Transaction};
use std::path::PathBuf;
@ -10,15 +10,16 @@ use crate::storage_error::StorageError;
const DB_NAME: &str = "messages";
/// A simple r2d2 manager for rusqlite connections.
pub struct SqliteManager;
pub struct SqliteManager {
database_path: PathBuf,
}
impl ManageConnection for SqliteManager {
type Connection = Connection;
type Error = rusqlite::Error;
fn connect(&self) -> Result<Connection, rusqlite::Error> {
let path = db_file_path(DB_NAME);
let conn = Connection::open(path)?;
let conn = Connection::open(&self.database_path)?;
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;")?;
conn.busy_timeout(Duration::from_millis(250))?;
Ok(conn)
@ -33,18 +34,43 @@ impl ManageConnection for SqliteManager {
}
}
static POOL: Lazy<Arc<r2d2::Pool<SqliteManager>>> = Lazy::new(|| {
let manager = SqliteManager;
static POOL: OnceCell<Arc<r2d2::Pool<SqliteManager>>> = OnceCell::new();
fn create_pool(database_path: PathBuf) -> Result<Arc<r2d2::Pool<SqliteManager>>, StorageError> {
create_pool_with_timeout(database_path, Duration::from_secs(30))
}
fn create_pool_with_timeout(
database_path: PathBuf,
connection_timeout: Duration,
) -> Result<Arc<r2d2::Pool<SqliteManager>>, StorageError> {
let manager = SqliteManager { database_path };
let pool = r2d2::Pool::builder()
.max_size(8)
.connection_timeout(connection_timeout)
.build(manager)
.expect("Failed to create database connection pool");
run_migrations(&pool).expect("Failed to run database migrations");
Arc::new(pool)
});
.map_err(|error| StorageError::Pool(error.to_string()))?;
run_migrations(&pool)?;
Ok(Arc::new(pool))
}
pub fn pool() -> Arc<r2d2::Pool<SqliteManager>> {
POOL.clone()
/// Opens the SQLite pool and applies all schema migrations.
///
/// Daemon startup calls this after database verification and before storage is
/// reported healthy, so connection and migration failures become a storage
/// component failure instead of a lazy-initialization panic.
pub fn initialize_database() -> Result<(), StorageError> {
let storage_dir = iota_util::file_util::storage_directory();
std::fs::create_dir_all(&storage_dir)?;
let database_path = storage_dir.join(format!("{DB_NAME}.sqlite3"));
POOL.get_or_try_init(|| create_pool(database_path))?;
Ok(())
}
pub fn pool() -> Result<Arc<r2d2::Pool<SqliteManager>>, StorageError> {
POOL.get()
.cloned()
.ok_or_else(|| StorageError::Other("database has not been initialized".into()))
}
pub fn with_db<T, F>(f: F) -> Result<T, StorageError>
@ -52,7 +78,8 @@ where
F: FnOnce(&Connection) -> Result<T, StorageError>,
{
blocking_region(|| {
let conn = POOL.get().map_err(|e| StorageError::Pool(e.to_string()))?;
let pool = pool()?;
let conn = pool.get().map_err(|e| StorageError::Pool(e.to_string()))?;
f(&conn)
})
}
@ -62,7 +89,8 @@ where
F: FnOnce(&Transaction<'_>) -> Result<T, StorageError>,
{
blocking_region(|| {
let mut conn = POOL.get().map_err(|e| StorageError::Pool(e.to_string()))?;
let pool = pool()?;
let mut conn = pool.get().map_err(|e| StorageError::Pool(e.to_string()))?;
let tx = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
let value = f(&tx)?;
tx.commit()?;
@ -962,6 +990,18 @@ pub fn create_general_messages_db() -> Result<Arc<std::sync::Mutex<Connection>>,
mod tests {
use super::*;
#[test]
fn pool_creation_failure_is_returned() -> Result<(), StorageError> {
let not_a_directory =
std::env::temp_dir().join(format!("iota-storage-pool-test-{}", std::process::id()));
std::fs::File::create(&not_a_directory)?;
let database_path = not_a_directory.join("messages.sqlite3");
assert!(create_pool_with_timeout(database_path, Duration::from_millis(1)).is_err());
std::fs::remove_file(not_a_directory)?;
Ok(())
}
#[test]
fn resumes_migration_when_height_exists_before_its_version() -> Result<(), StorageError> {
let conn = Connection::open_in_memory()?;