v0.2.23
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-04 20:44:31 +01:00
1 parent 3b9110c8c8
commit beae94e932
42 files changed
+2518 -274

No files matched your search

+106 -58
View File
@@ -1,6 +1,53 @@
use crate::db::pool::{Connection, DbValue, ToDbValue};
use serde::{Deserialize, Deserializer, Serialize};
/// Every predefined interface style. `default` (follow the server-wide
/// `default_interface_skin`) is accepted as a user preference in addition to
/// these, but is not itself a style. Shared with configuration validation so
/// the server default and the per-user preference can never drift apart.
pub const INTERFACE_SKINS: &[&str] = &[
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
"dracula",
"catppuccin",
"solarized",
"gruvbox",
"tokyo-night",
"rose-pine",
"everforest",
"synthwave",
"paper",
"terminal",
"aqua",
"linear",
"material",
"retro",
];
/// How far a predefined interface style may reshape the interface.
pub const SKIN_DEPTHS: &[&str] = &["colors", "standard", "immersive"];
#[derive(Debug, Clone, Serialize)]
pub struct DisplayPreferences {
pub density: String,
@@ -56,6 +103,8 @@ pub struct DisplayPreferences {
pub interface_skin: String,
pub preset_scale_unlocked: bool,
pub adaptive_email_colors: bool,
pub skin_depth: String,
pub skin_gradients: bool,
pub updated_at: String,
}
@@ -115,6 +164,8 @@ pub struct UpdateDisplayPreferences {
pub interface_skin: Option<String>,
pub preset_scale_unlocked: Option<bool>,
pub adaptive_email_colors: Option<bool>,
pub skin_depth: Option<String>,
pub skin_gradients: Option<bool>,
}
fn deserialize_animation_mode_field<'de, D>(
@@ -182,6 +233,8 @@ fn defaults() -> DisplayPreferences {
interface_skin: "default".into(),
preset_scale_unlocked: false,
adaptive_email_colors: false,
skin_depth: "standard".into(),
skin_gradients: false,
updated_at: String::new(),
}
}
@@ -242,7 +295,9 @@ pub fn get_preferences(conn: &Connection) -> Result<DisplayPreferences, String>
interface_skin: row.get(50)?,
preset_scale_unlocked: row.get::<_, i32>(51)? != 0,
adaptive_email_colors: row.get::<_, i32>(52)? != 0,
updated_at: row.get(53)?,
skin_depth: row.get(53)?,
skin_gradients: row.get::<_, i32>(54)? != 0,
updated_at: row.get(55)?,
})
});
match result {
@@ -380,37 +435,13 @@ pub fn update_preferences(
if let Some(ref v) = data.forward_mode {
enum_value("forward_mode", v, &["inline", "attachment"])?;
}
if let Some(ref v) = data.interface_skin {
enum_value(
"interface_skin",
v,
&[
"default",
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
],
)?;
if let Some(ref v) = data.interface_skin
&& v != "default"
{
enum_value("interface_skin", v, INTERFACE_SKINS)?;
}
if let Some(ref v) = data.skin_depth {
enum_value("skin_depth", v, SKIN_DEPTHS)?;
}
if let Some(v) = data.font_size {
range("font_size", v, 11, 18)?;
@@ -544,6 +575,8 @@ pub fn update_preferences(
set_text!(interface_skin, "interface_skin");
set_bool!(preset_scale_unlocked, "preset_scale_unlocked");
set_bool!(adaptive_email_colors, "adaptive_email_colors");
set_text!(skin_depth, "skin_depth");
set_bool!(skin_gradients, "skin_gradients");
if sets.is_empty() {
return get_preferences(conn);
@@ -610,32 +643,7 @@ mod tests {
#[test]
fn built_in_interface_skins_are_accepted() {
for skin in [
"default",
"rustbox",
"roundcube-classic",
"roundcube-larry",
"roundcube-elastic",
"clean",
"gmail",
"proton",
"zoho",
"outlook",
"minimal",
"graphite",
"high-contrast",
"fastmail",
"apple-mail",
"yahoo",
"hey",
"nord",
"github",
"gitlab",
"discord",
"slack",
"reddit",
"notion",
] {
for skin in std::iter::once("default").chain(INTERFACE_SKINS.iter().copied()) {
let conn = open_test_db();
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"interface_skin": skin})).unwrap();
@@ -660,4 +668,44 @@ mod tests {
serde_json::from_value(serde_json::json!({"accent_color":"red"})).unwrap();
assert!(update_preferences(&conn, &update).is_err());
}
#[test]
fn skin_depth_defaults_to_standard_and_roundtrips() {
let conn = open_test_db();
assert_eq!(get_preferences(&conn).unwrap().skin_depth, "standard");
for depth in SKIN_DEPTHS {
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_depth": depth})).unwrap();
assert_eq!(
update_preferences(&conn, &update).unwrap().skin_depth,
*depth
);
}
}
#[test]
fn skin_gradients_default_off_and_roundtrip() {
let conn = open_test_db();
assert!(!get_preferences(&conn).unwrap().skin_gradients);
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_gradients": true})).unwrap();
assert!(update_preferences(&conn, &update).unwrap().skin_gradients);
}
#[test]
fn invalid_skin_depth_is_rejected() {
let conn = open_test_db();
let update: UpdateDisplayPreferences =
serde_json::from_value(serde_json::json!({"skin_depth":"total"})).unwrap();
assert!(update_preferences(&conn, &update).is_err());
}
#[test]
fn interface_skin_catalog_has_no_duplicates_or_default() {
let mut seen = std::collections::HashSet::new();
for skin in INTERFACE_SKINS {
assert_ne!(*skin, "default");
assert!(seen.insert(*skin), "duplicate interface skin {skin}");
}
}
}
+39 -52
View File
@@ -16,38 +16,23 @@ use tokio::sync::Mutex as AsyncMutex;
use crate::config::AppConfig;
pub const BASE_SCHEMA_VERSION: u32 = 50;
pub const BASE_SCHEMA_VERSION: u32 = 52;
const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/sqlite/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/sqlite/V052__snooze.sql")),
];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/mysql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/mysql/V052__snooze.sql")),
];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[
(
51,
include_str!(
"../../migrations/pgsql/V051__adaptive_email_colors_and_message_loading_mode.sql"
),
),
(52, include_str!("../../migrations/pgsql/V052__snooze.sql")),
];
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/sqlite/V053__skin_depth_and_gradients.sql"),
)];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/mysql/V053__skin_depth_and_gradients.sql"),
)];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
53,
include_str!("../../migrations/pgsql/V053__skin_depth_and_gradients.sql"),
)];
const TENANT_TABLES: &[&str] = &[
"blob_delete_outbox",
@@ -1836,7 +1821,7 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
if version < BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
"legacy SQLite schema version {version} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V{BASE_SCHEMA_VERSION:03}-capable RustBox release first"
));
}
let latest_supported_sqlite = SQLITE_MIGRATIONS
@@ -2197,7 +2182,7 @@ fn apply_sqlite_migrations(
) -> Result<(), String> {
if current < BASE_SCHEMA_VERSION {
return Err(format!(
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V050-capable RustBox release first"
"database schema version {current} is older than the consolidated RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with a V{BASE_SCHEMA_VERSION:03}-capable RustBox release first"
));
}
let latest = latest_schema_version();
@@ -2642,19 +2627,17 @@ mod tests {
}
#[test]
fn sqlite_v051_migrates_existing_v050_preferences() {
fn sqlite_v053_adds_skin_depth_and_gradients_to_existing_baseline_tenants() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE rustbox_accounts (user_hash TEXT PRIMARY KEY, table_prefix TEXT NOT NULL, schema_version INTEGER NOT NULL, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);\n\
CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY, theme TEXT NOT NULL DEFAULT 'system');\n\
CREATE TABLE u_test__mail_preferences (id INTEGER PRIMARY KEY, messages_per_page INTEGER NOT NULL DEFAULT 300);\n\
INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES('test', 'u_test__', 50);\n\
INSERT INTO u_test__display_preferences(id) VALUES(1);\n\
INSERT INTO u_test__mail_preferences(id) VALUES(1);",
CREATE TABLE u_test__display_preferences (id INTEGER PRIMARY KEY, theme TEXT NOT NULL DEFAULT 'system', interface_skin TEXT NOT NULL DEFAULT 'rustbox');\n\
INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES('test', 'u_test__', 52);\n\
INSERT INTO u_test__display_preferences(id, interface_skin) VALUES(1, 'nord');",
)
.unwrap();
apply_sqlite_migrations(&conn, "u_test__", 50).unwrap();
apply_sqlite_migrations(&conn, "u_test__", BASE_SCHEMA_VERSION).unwrap();
let version: u32 = conn
.query_row(
@@ -2663,28 +2646,30 @@ mod tests {
|row| row.get(0),
)
.unwrap();
assert_eq!(version, 52);
assert_eq!(version, latest_schema_version());
let adaptive: i64 = conn
let (skin, depth, gradients): (String, String, i64) = conn
.query_row(
"SELECT adaptive_email_colors FROM u_test__display_preferences WHERE id=1",
"SELECT interface_skin, skin_depth, skin_gradients FROM u_test__display_preferences WHERE id=1",
[],
|row| row.get(0),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
)
.unwrap();
let mode: String = conn
.query_row(
"SELECT message_loading_mode FROM u_test__mail_preferences WHERE id=1",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(adaptive, 0);
assert_eq!(mode, "pagination");
assert_eq!(skin, "nord");
assert_eq!(depth, "standard");
assert_eq!(gradients, 0);
}
#[test]
fn legacy_sqlite_v050_is_imported_and_backed_up() {
fn sqlite_migrations_reject_tenants_older_than_the_baseline() {
let conn = rusqlite::Connection::open_in_memory().unwrap();
let error =
apply_sqlite_migrations(&conn, "u_test__", BASE_SCHEMA_VERSION - 1).unwrap_err();
assert!(error.contains("older than the consolidated RustBox baseline"));
}
#[test]
fn legacy_sqlite_baseline_is_imported_and_backed_up() {
let temp = tempfile::tempdir().unwrap();
let data_dir = temp.path();
let user_hash = format!("{:x}", Sha256::digest(b"legacy-user"));
@@ -2700,7 +2685,9 @@ mod tests {
legacy
.execute_batch(&tenant_seed_sql(DatabaseBackend::Sqlite, ""))
.unwrap();
legacy.execute_batch("PRAGMA user_version=50;").unwrap();
legacy
.execute_batch(&format!("PRAGMA user_version={BASE_SCHEMA_VERSION};"))
.unwrap();
legacy.execute(
"INSERT INTO display_preferences(id, theme, accent_color) VALUES(1, 'dark', '#123456')",
[],