use crate::{error::VaultError, registry::Vault};
use gradatum_core::{frontmatter::Frontmatter, identity::NoteId};
#[derive(Debug, PartialEq)]
pub enum WriteResult {
Written {
new_sha256: [u8; 32],
},
Conflict {
current_sha256: [u8; 32],
},
}
impl Vault {
pub async fn write_if_match(
&self,
frontmatter: Frontmatter,
body: String,
id: NoteId,
expected_sha256: Option<[u8; 32]>,
) -> Result<WriteResult, VaultError> {
if let Some(expected) = expected_sha256 {
match self.read_note(id).await {
Ok(existing) => {
let current = existing.content_hash.0;
if current != expected {
return Ok(WriteResult::Conflict {
current_sha256: current,
});
}
}
Err(VaultError::Core(gradatum_core::error::GradatumError::NoteNotFound(_))) => {
}
Err(other) => return Err(other),
}
}
let written = self.write_note_inner(frontmatter, body, id).await?;
Ok(WriteResult::Written {
new_sha256: written.content_hash.0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use gradatum_core::frontmatter::Frontmatter;
use gradatum_core::scope::VaultId;
use gradatum_core::section::Section;
use gradatum_core::status::NoteStatus;
use tempfile::TempDir;
fn minimal_fm() -> Frontmatter {
Frontmatter {
schema_version: 1,
vault_id: VaultId::new("main"),
locus: None,
section: Section::Decisions,
status: NoteStatus::Draft,
status_reason: None,
status_changed: None,
tags: Default::default(),
author: None,
created: Utc::now(),
updated: None,
extra: Default::default(),
provenance: None,
forgotten: None,
forgotten_at: None,
forgotten_by: None,
}
}
#[tokio::test]
async fn write_if_match_none_is_unconditional() {
let dir = TempDir::new().unwrap();
let vault = Vault::create(dir.path(), VaultId::new("main"))
.await
.unwrap();
let id = NoteId::new();
let result = vault
.write_if_match(minimal_fm(), "corps".into(), id, None)
.await
.unwrap();
assert!(
matches!(result, WriteResult::Written { .. }),
"None doit retourner Written"
);
}
#[tokio::test]
async fn write_if_match_correct_hash_writes() {
let dir = TempDir::new().unwrap();
let vault = Vault::create(dir.path(), VaultId::new("main"))
.await
.unwrap();
let id = NoteId::new();
let r1 = vault
.write_if_match(minimal_fm(), "v1".into(), id, None)
.await
.unwrap();
let WriteResult::Written {
new_sha256: hash_v1,
} = r1
else {
panic!("premier write doit retourner Written");
};
let r2 = vault
.write_if_match(minimal_fm(), "v2".into(), id, Some(hash_v1))
.await
.unwrap();
assert!(
matches!(r2, WriteResult::Written { .. }),
"hash courant correct doit retourner Written"
);
}
#[tokio::test]
async fn write_if_match_conflict_on_stale_hash() {
let dir = TempDir::new().unwrap();
let vault = Vault::create(dir.path(), VaultId::new("main"))
.await
.unwrap();
let id = NoteId::new();
let r1 = vault
.write_if_match(minimal_fm(), "v1".into(), id, None)
.await
.unwrap();
let WriteResult::Written {
new_sha256: hash_v1,
} = r1
else {
panic!("premier write doit retourner Written");
};
vault
.write_if_match(minimal_fm(), "v2".into(), id, None)
.await
.unwrap();
let r3 = vault
.write_if_match(minimal_fm(), "v3".into(), id, Some(hash_v1))
.await
.unwrap();
match r3 {
WriteResult::Conflict { current_sha256 } => {
assert_ne!(
current_sha256, hash_v1,
"current_sha256 sur Conflict doit être celui de v2"
);
}
WriteResult::Written { .. } => panic!("hash périmé doit retourner Conflict"),
}
let note = vault.read_note(id).await.unwrap();
assert_eq!(
note.body.markdown, "v2",
"note ne doit pas être écrasée par v3 sur Conflict"
);
}
#[tokio::test]
async fn write_if_match_new_note_with_expected_writes() {
let dir = TempDir::new().unwrap();
let vault = Vault::create(dir.path(), VaultId::new("main"))
.await
.unwrap();
let id = NoteId::new();
let arbitrary_hash = [0u8; 32];
let result = vault
.write_if_match(minimal_fm(), "nouveau".into(), id, Some(arbitrary_hash))
.await
.unwrap();
assert!(
matches!(result, WriteResult::Written { .. }),
"note nouvelle avec expected doit retourner Written"
);
}
}