Rust backend: SQLite (WAL mode, 8 tables), vault encryption (Argon2id + AES-256-GCM), settings/connections/credentials services, 19 Tauri command wrappers. 46/46 tests passing. Vue 3 frontend: unlock/create vault flow, Pinia app store, Tailwind CSS v4 dark theme with Wraith branding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
106 lines
3.0 KiB
Rust
106 lines
3.0 KiB
Rust
use crate::db::Database;
|
|
|
|
/// Key-value settings store backed by the `settings` SQLite table.
|
|
///
|
|
/// The table schema is:
|
|
/// key TEXT PRIMARY KEY
|
|
/// value TEXT NOT NULL
|
|
///
|
|
/// All operations acquire the shared DB mutex for their duration and
|
|
/// return immediately — no async needed for a local SQLite store.
|
|
pub struct SettingsService {
|
|
db: Database,
|
|
}
|
|
|
|
impl SettingsService {
|
|
pub fn new(db: Database) -> Self {
|
|
Self { db }
|
|
}
|
|
|
|
/// Retrieve the value for `key`, or `None` if it has never been set.
|
|
pub fn get(&self, key: &str) -> Option<String> {
|
|
let conn = self.db.conn();
|
|
conn.query_row(
|
|
"SELECT value FROM settings WHERE key = ?1",
|
|
rusqlite::params![key],
|
|
|row| row.get::<_, String>(0),
|
|
)
|
|
.ok()
|
|
}
|
|
|
|
/// Insert or update the value for `key` (SQLite UPSERT).
|
|
pub fn set(&self, key: &str, value: &str) -> Result<(), String> {
|
|
let conn = self.db.conn();
|
|
conn.execute(
|
|
"INSERT INTO settings (key, value) VALUES (?1, ?2)
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
rusqlite::params![key, value],
|
|
)
|
|
.map(|_| ())
|
|
.map_err(|e| format!("settings::set failed for key '{key}': {e}"))
|
|
}
|
|
|
|
/// Remove the entry for `key`. Succeeds silently if the key does not exist.
|
|
pub fn delete(&self, key: &str) -> Result<(), String> {
|
|
let conn = self.db.conn();
|
|
conn.execute(
|
|
"DELETE FROM settings WHERE key = ?1",
|
|
rusqlite::params![key],
|
|
)
|
|
.map(|_| ())
|
|
.map_err(|e| format!("settings::delete failed for key '{key}': {e}"))
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::db::Database;
|
|
|
|
fn make_service() -> SettingsService {
|
|
let db = Database::open(std::path::Path::new(":memory:")).unwrap();
|
|
db.migrate().unwrap();
|
|
SettingsService::new(db)
|
|
}
|
|
|
|
#[test]
|
|
fn get_missing_returns_none() {
|
|
let svc = make_service();
|
|
assert_eq!(svc.get("nonexistent"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn set_and_get_roundtrip() {
|
|
let svc = make_service();
|
|
svc.set("theme", "dracula").unwrap();
|
|
assert_eq!(svc.get("theme"), Some("dracula".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn set_overwrites_existing_value() {
|
|
let svc = make_service();
|
|
svc.set("theme", "dracula").unwrap();
|
|
svc.set("theme", "solarized").unwrap();
|
|
assert_eq!(svc.get("theme"), Some("solarized".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn delete_removes_key() {
|
|
let svc = make_service();
|
|
svc.set("to_remove", "yes").unwrap();
|
|
svc.delete("to_remove").unwrap();
|
|
assert_eq!(svc.get("to_remove"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_nonexistent_key_succeeds() {
|
|
let svc = make_service();
|
|
// Should not return an error.
|
|
assert!(svc.delete("ghost").is_ok());
|
|
}
|
|
}
|