use rusqlite::params;
use super::{
EnvId, ProjectId, Vault,
error::{DbError, map_rusqlite_error},
};
#[derive(Debug, Clone)]
pub struct EnvironmentStatus {
pub id: EnvId,
pub name: String,
pub secret_count: i64,
pub last_modified_at: Option<i64>,
pub sealed_at: Option<i64>,
}
impl Vault {
pub fn upsert_sync_marker(&self, env_id: &EnvId, sealed_at: i64) -> Result<(), DbError> {
self.conn
.execute(
"INSERT OR REPLACE INTO sync_markers (environment_id, sealed_at)
VALUES (?1, ?2)",
params![env_id.as_str(), sealed_at],
)
.map_err(map_rusqlite_error)?;
Ok(())
}
pub fn environment_status(
&self,
project_id: &ProjectId,
) -> Result<Vec<EnvironmentStatus>, DbError> {
let mut stmt = self
.conn
.prepare(
"SELECT
e.id,
e.name,
COUNT(s.id) AS secret_count,
MAX(s.updated_at) AS last_modified_at,
sm.sealed_at
FROM environments e
LEFT JOIN secrets s
ON s.environment_id = e.id
LEFT JOIN sync_markers sm
ON sm.environment_id = e.id
WHERE e.project_id = ?1
GROUP BY e.id, e.name, sm.sealed_at
ORDER BY e.name ASC",
)
.map_err(map_rusqlite_error)?;
let rows = stmt
.query_map(params![project_id.as_str()], |row| {
Ok(EnvironmentStatus {
id: EnvId(row.get(0)?),
name: row.get(1)?,
secret_count: row.get(2)?,
last_modified_at: row.get(3)?,
sealed_at: row.get(4)?,
})
})
.map_err(map_rusqlite_error)?;
rows.map(|r| r.map_err(map_rusqlite_error))
.collect::<Result<Vec<EnvironmentStatus>, DbError>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_KEY: [u8; 32] = [0xABu8; 32];
fn open_vault() -> (tempfile::TempDir, Vault, crate::db::ProjectId) {
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("vault.db");
let vault = Vault::open(&path, &TEST_KEY).expect("vault open");
let pid = vault
.create_project("test-project")
.expect("create project");
(tmp, vault, pid)
}
#[test]
fn schema_v2_migration_adds_sync_markers_table() {
let (_tmp, vault, _pid) = open_vault();
assert!(
vault
.table_exists("sync_markers")
.expect("table_exists must not fail"),
"sync_markers table must exist after V2 migration"
);
}
#[test]
fn upsert_sync_marker_creates_row() {
let (_tmp, vault, pid) = open_vault();
let env_id = vault
.create_environment(&pid, "development")
.expect("create env");
vault
.upsert_sync_marker(&env_id, 1_000)
.expect("upsert must succeed");
let sealed_at: i64 = vault
.conn
.query_row(
"SELECT sealed_at FROM sync_markers WHERE environment_id = ?1",
params![env_id.as_str()],
|row| row.get(0),
)
.expect("row must exist");
assert_eq!(sealed_at, 1_000);
}
#[test]
fn upsert_sync_marker_updates_existing_row() {
let (_tmp, vault, pid) = open_vault();
let env_id = vault
.create_environment(&pid, "development")
.expect("create env");
vault
.upsert_sync_marker(&env_id, 1_000)
.expect("first upsert");
vault
.upsert_sync_marker(&env_id, 2_000)
.expect("second upsert");
let count: i64 = vault
.conn
.query_row(
"SELECT COUNT(*) FROM sync_markers WHERE environment_id = ?1",
params![env_id.as_str()],
|row| row.get(0),
)
.expect("count must succeed");
assert_eq!(count, 1, "only one row must exist after two upserts");
let sealed_at: i64 = vault
.conn
.query_row(
"SELECT sealed_at FROM sync_markers WHERE environment_id = ?1",
params![env_id.as_str()],
|row| row.get(0),
)
.expect("row must exist");
assert_eq!(
sealed_at, 2_000,
"sealed_at must be updated to the latest value"
);
}
#[test]
fn environment_status_returns_never_sealed_when_no_marker() {
let (_tmp, vault, pid) = open_vault();
vault
.create_environment(&pid, "development")
.expect("create env");
let env_id = vault
.get_environment_by_name(&pid, "development")
.expect("env must exist")
.id;
vault
.upsert_secret(&env_id, "MY_KEY", &[0u8; 32], &[0u8; 12])
.expect("upsert secret");
let statuses = vault.environment_status(&pid).expect("env_status");
assert_eq!(statuses.len(), 1);
assert_eq!(statuses[0].secret_count, 1);
assert!(
statuses[0].sealed_at.is_none(),
"sealed_at must be None when no sync marker exists"
);
}
#[test]
fn environment_status_returns_correct_last_modified_at() {
let (_tmp, vault, pid) = open_vault();
let env_id = vault
.create_environment(&pid, "development")
.expect("create env");
vault
.upsert_secret(&env_id, "KEY", &[0u8; 32], &[0u8; 12])
.expect("upsert secret");
let statuses = vault.environment_status(&pid).expect("env_status");
assert_eq!(statuses.len(), 1);
assert!(
statuses[0].last_modified_at.is_some(),
"last_modified_at must be Some when secrets exist"
);
}
#[test]
fn environment_status_zero_secrets_returns_none_last_modified() {
let (_tmp, vault, pid) = open_vault();
vault
.create_environment(&pid, "empty-env")
.expect("create env");
let statuses = vault.environment_status(&pid).expect("env_status");
assert_eq!(statuses.len(), 1);
assert_eq!(statuses[0].secret_count, 0);
assert!(
statuses[0].last_modified_at.is_none(),
"last_modified_at must be None when there are no secrets"
);
}
#[test]
fn environment_status_returns_multiple_envs_sorted_by_name() {
let (_tmp, vault, pid) = open_vault();
vault
.create_environment(&pid, "zebra")
.expect("create zebra");
vault
.create_environment(&pid, "alpha")
.expect("create alpha");
let statuses = vault.environment_status(&pid).expect("env_status");
assert_eq!(statuses.len(), 2);
assert_eq!(statuses[0].name, "alpha");
assert_eq!(statuses[1].name, "zebra");
}
#[test]
fn sync_marker_deleted_on_environment_cascade() {
let (_tmp, vault, pid) = open_vault();
let env_id = vault
.create_environment(&pid, "development")
.expect("create env");
vault
.upsert_sync_marker(&env_id, 1_000)
.expect("upsert marker");
vault
.delete_environment(&env_id)
.expect("delete environment");
let count: i64 = vault
.conn
.query_row(
"SELECT COUNT(*) FROM sync_markers WHERE environment_id = ?1",
params![env_id.as_str()],
|row| row.get(0),
)
.expect("count must succeed");
assert_eq!(
count, 0,
"sync_marker must be deleted by CASCADE when env is deleted"
);
}
}