// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Boundary tests for the oakstorage write-through façade
//! (`oak_storage::writethrough`): configuration gating, binding lifecycle,
//! write-through / flush / unbind, error recording, and the null-handle
//! contract.
//!
//! The config store is process-global, so all tests serialize on a local
//! mutex and write `OAK_CONFIG_DIR` to an isolated temp directory before
//! the first settings access (the tests never call `ConfigStore::save`, so
//! nothing persists).
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use oak_node::project::Project;
use oak_storage::handle::CHandle;
use oak_storage::nodeutil::make_project_owned;
use oak_storage::uri::StorageUri;
use oak_storage::writethrough;
/// Serializes the tests (global config + global bindings + the snapshot
/// thread).
static CONFIG_LOCK: Mutex<()> = Mutex::new(());
/// Take the lock and isolate the config store under a per-process temp
/// directory (once) before any settings access.
fn isolated_config() -> MutexGuard<'static, ()> {
static INIT: OnceLock<()> = OnceLock::new();
let guard = CONFIG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
INIT.get_or_init(|| {
let dir = std::env::temp_dir().join(format!(
"oakstorage_writethrough_config_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::create_dir_all(&dir);
std::env::set_var("OAK_CONFIG_DIR", &dir);
});
guard
}
fn set_storage_config(backend: &str, sqlite_path: Option<&std::path::Path>, pg_url: Option<&str>) {
let store = oak_core::configstore::ConfigStore::instance();
store.set(Some("Storage"), "Backend", backend);
store.set(
Some("Storage"),
"SqlitePath",
sqlite_path
.map(|p| p.to_string_lossy().into_owned())
.as_deref()
.unwrap_or(""),
);
store.set(Some("Storage"), "PgUrl", pg_url.unwrap_or(""));
}
fn work_dir(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"oakstorage_writethrough_{tag}_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("work dir");
dir
}
fn project_handle() -> (CHandle, Arc>) {
let project = Project::new();
let handle = make_project_owned(project.clone());
(handle, project)
}
fn release(handle: CHandle) {
if let Some(release) = handle.release {
// SAFETY: the handle is an owned box (refcount 1) that this test
// created via `make_project_owned`.
unsafe { release(handle.ctx) };
}
}
// ---- null handles and status ------------------------------------------
#[test]
fn null_handles_are_safe_no_ops() {
let _guard = isolated_config();
assert!(!writethrough::is_bound(CHandle::null()));
assert_eq!(writethrough::last_error(CHandle::null()), None);
writethrough::bind_project(CHandle::null());
writethrough::unbind_project(CHandle::null());
// Nothing to flush is a no-op (and idempotent).
writethrough::flush_all();
writethrough::flush_all();
}
// ---- configuration gating ---------------------------------------------
#[test]
fn backend_disabled_by_config_leaves_projects_unbound() {
let _guard = isolated_config();
set_storage_config("off", None, None);
assert!(!writethrough::storage_enabled());
let (handle, _project) = project_handle();
writethrough::bind_project(handle);
assert!(
!writethrough::is_bound(handle),
"disabled backend must not bind"
);
writethrough::unbind_project(handle);
release(handle);
}
#[test]
fn backend_enabled_by_config_binds_and_round_trips() {
let _guard = isolated_config();
let dir = work_dir("sq");
let db = dir.join("library.db");
set_storage_config("sqlite", Some(&db), None);
assert!(writethrough::storage_enabled());
let (handle, project) = project_handle();
assert!(!writethrough::is_bound(handle));
writethrough::bind_project(handle);
assert!(writethrough::is_bound(handle));
assert_eq!(writethrough::last_error(handle), None);
// Re-binding is idempotent (the existing binding is kept).
writethrough::bind_project(handle);
assert!(writethrough::is_bound(handle));
// Add real state so the round trip carries more than an empty row.
let original_uuid = {
let mut guard = project.lock().unwrap_or_else(|e| e.into_inner());
let (core, behavior) = oak_node::folder::create("Written Through");
guard.graph.add_node(core, behavior);
guard.uuid.clone()
};
// A command-success notification writes the project through.
writethrough::note_command();
assert_eq!(
writethrough::last_error(handle),
None,
"a write-through to a writable temp path succeeds"
);
// The write-through is real: the sqlite library file exists...
assert!(
db.is_file(),
"the sqlite library file was created at {}",
db.display()
);
assert!(
std::fs::metadata(&db).map(|m| m.len()).unwrap_or(0) > 0,
"the sqlite library file is non-empty"
);
// Flush drains and snapshots; it is idempotent and safe afterwards.
writethrough::flush_all();
writethrough::flush_all();
// ...and a fresh backend load reads the bound project's state back.
let uri = StorageUri::parse(&writethrough::library_uri().expect("library uri"))
.expect("parse library uri");
let loaded = writethrough::backend()
.load_project(&uri)
.expect("load the written-through project");
{
let guard = loaded.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(
guard.uuid, original_uuid,
"the loaded library row is the bound project"
);
let labels: Vec = guard
.graph
.node_ids()
.into_iter()
.filter_map(|id| guard.graph.get(id))
.map(|entry| entry.core.label.clone())
.collect();
assert!(
labels.iter().any(|label| label == "Written Through"),
"the loaded project carries the state added before note_command: {labels:?}"
);
}
writethrough::unbind_project(handle);
assert!(!writethrough::is_bound(handle), "unbind drops the binding");
assert_eq!(writethrough::last_error(handle), None);
release(handle);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn write_through_failure_is_recorded_in_last_error() {
let _guard = isolated_config();
// A directory cannot be opened as a SQLite database: the write-through
// fails and the binding records the error instead of panicking.
let dir = work_dir("bad");
set_storage_config("sqlite", Some(&dir), None);
let (handle, _project) = project_handle();
writethrough::bind_project(handle);
assert!(writethrough::is_bound(handle));
writethrough::note_command();
assert!(
writethrough::last_error(handle).is_some(),
"a failing write-through must record last_error"
);
writethrough::flush_all();
writethrough::unbind_project(handle);
release(handle);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn library_uri_resolves_sqlite_and_pg_configurations() {
let _guard = isolated_config();
let dir = work_dir("uri");
let db = dir.join("lib.db");
set_storage_config("sqlite", Some(&db), None);
let uri = writethrough::library_uri().expect("sqlite uri");
assert!(uri.starts_with("oakdb+sqlite://"), "{uri}");
assert!(uri.ends_with("lib.db"), "{uri}");
// PostgreSQL: the postgres:// scheme is stripped into the oakdb body.
set_storage_config(
"pg",
None,
Some("postgres://user:pass@host:5432/db"),
);
assert_eq!(
writethrough::library_uri().as_deref(),
Some("oakdb+pg://user:pass@host:5432/db")
);
// `postgresql://` is accepted too.
set_storage_config("pg", None, Some("postgresql://user@host/db"));
assert_eq!(
writethrough::library_uri().as_deref(),
Some("oakdb+pg://user@host/db")
);
// pg selected without a URL: no library configured.
set_storage_config("pg", None, None);
assert_eq!(writethrough::library_uri(), None);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn default_library_path_honors_the_config_directory() {
let _guard = isolated_config();
let path = writethrough::default_library_path();
assert!(path.ends_with("library.db"), "{path}");
let config_dir = std::env::var("OAK_CONFIG_DIR").expect("test isolation set it");
assert!(
path.starts_with(&config_dir),
"{path} must live under {config_dir}"
);
}
#[test]
fn note_command_without_bindings_is_a_no_op() {
let _guard = isolated_config();
set_storage_config(
"sqlite",
Some(&work_dir("none").join("lib.db")),
None,
);
// No project is bound in this process (the other tests' bindings are
// dropped before they return): the notification must not panic.
writethrough::note_command();
writethrough::flush_all();
}