feat(sync): include notes in snapshots
This commit is contained in:
@@ -30,6 +30,7 @@ mod space_exports;
|
|||||||
mod spaces;
|
mod spaces;
|
||||||
mod splits;
|
mod splits;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
mod sync_notes;
|
||||||
mod tab_group_order;
|
mod tab_group_order;
|
||||||
mod tab_groups;
|
mod tab_groups;
|
||||||
mod tab_lifecycle;
|
mod tab_lifecycle;
|
||||||
|
|||||||
@@ -120,6 +120,9 @@ impl BrowserCore {
|
|||||||
for record in body.bookmarks {
|
for record in body.bookmarks {
|
||||||
self.apply_bookmark_sync_record(record, &mut summary)?;
|
self.apply_bookmark_sync_record(record, &mut summary)?;
|
||||||
}
|
}
|
||||||
|
for record in body.notes {
|
||||||
|
self.apply_note_sync_record(record, &mut summary)?;
|
||||||
|
}
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +399,7 @@ impl BrowserCore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_profile_id(&self, raw: &str) -> Result<ProfileId, SyncClientError> {
|
pub(super) fn sync_profile_id(&self, raw: &str) -> Result<ProfileId, SyncClientError> {
|
||||||
let profile_id = ProfileId::parse(raw).map_err(snapshot_schema_error)?;
|
let profile_id = ProfileId::parse(raw).map_err(snapshot_schema_error)?;
|
||||||
if self.profiles.iter().any(|profile| profile.id() == &profile_id) {
|
if self.profiles.iter().any(|profile| profile.id() == &profile_id) {
|
||||||
return Ok(profile_id);
|
return Ok(profile_id);
|
||||||
@@ -404,7 +407,7 @@ impl BrowserCore {
|
|||||||
Ok(self.active_profile_id.clone())
|
Ok(self.active_profile_id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_space_id(
|
pub(super) fn sync_space_id(
|
||||||
&self,
|
&self,
|
||||||
raw: &str,
|
raw: &str,
|
||||||
space_name: Option<&str>,
|
space_name: Option<&str>,
|
||||||
@@ -460,6 +463,6 @@ fn parse_space_id(raw: &str) -> Result<SpaceId, SyncClientError> {
|
|||||||
SpaceId::parse(raw).map_err(snapshot_schema_error)
|
SpaceId::parse(raw).map_err(snapshot_schema_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snapshot_schema_error(error: impl ToString) -> SyncClientError {
|
pub(super) fn snapshot_schema_error(error: impl ToString) -> SyncClientError {
|
||||||
SyncClientError::SnapshotSchema(error.to_string())
|
SyncClientError::SnapshotSchema(error.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
use std::time::{Duration, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use ely_domain::{
|
||||||
|
NoteEntry, NoteId, NoteTarget, ProfileId, SpaceId, SyncObjectKind, SyncObjectPolicy, TabId,
|
||||||
|
UrlText,
|
||||||
|
};
|
||||||
|
use ely_sync_client::SyncClientError;
|
||||||
|
|
||||||
|
use super::{BrowserCore, sync::snapshot_schema_error};
|
||||||
|
use crate::{
|
||||||
|
sync_engine::SyncSnapshotApplySummary,
|
||||||
|
sync_records::{NoteSyncRecord, NoteTargetSyncRecord},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl BrowserCore {
|
||||||
|
pub(crate) fn visible_notes_for_sync(&self) -> Vec<&NoteEntry> {
|
||||||
|
if self.sync_object_policy(SyncObjectKind::Notes) == SyncObjectPolicy::Paused {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
self.notes
|
||||||
|
.iter()
|
||||||
|
.filter(|note| {
|
||||||
|
self.profiles
|
||||||
|
.iter()
|
||||||
|
.any(|profile| profile.id() == note.profile_id() && profile.allows_sync())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn apply_note_sync_record(
|
||||||
|
&mut self,
|
||||||
|
record: NoteSyncRecord,
|
||||||
|
summary: &mut SyncSnapshotApplySummary,
|
||||||
|
) -> Result<(), SyncClientError> {
|
||||||
|
let note_id = NoteId::parse(&record.id).map_err(snapshot_schema_error)?;
|
||||||
|
let profile_id = self.sync_profile_id(&record.profile_id)?;
|
||||||
|
let space_id = self.sync_space_id(&record.space_id, record.space_name.as_deref())?;
|
||||||
|
let source_url = UrlText::parse(&record.source_url).map_err(snapshot_schema_error)?;
|
||||||
|
let target =
|
||||||
|
self.resolve_note_sync_target(record.target, &profile_id, &space_id, &source_url)?;
|
||||||
|
let created_at = UNIX_EPOCH + Duration::from_secs(record.created_at_secs);
|
||||||
|
let updated_at = UNIX_EPOCH + Duration::from_secs(record.updated_at_secs);
|
||||||
|
let existing_index = self.note_sync_index(¬e_id, &profile_id, &target);
|
||||||
|
let id = existing_index
|
||||||
|
.and_then(|index| self.notes.get(index).map(|note| note.id().clone()))
|
||||||
|
.unwrap_or(note_id);
|
||||||
|
let note = NoteEntry::new(
|
||||||
|
profile_id,
|
||||||
|
space_id,
|
||||||
|
target,
|
||||||
|
record.title,
|
||||||
|
source_url,
|
||||||
|
record.body,
|
||||||
|
created_at,
|
||||||
|
)
|
||||||
|
.map_err(snapshot_schema_error)?;
|
||||||
|
let note = NoteEntry::restore(id, note, updated_at);
|
||||||
|
|
||||||
|
match existing_index {
|
||||||
|
Some(index) if self.notes[index] == note => summary.record_skipped(),
|
||||||
|
Some(index) => {
|
||||||
|
self.notes[index] = note;
|
||||||
|
summary.record_updated();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.notes.push(note);
|
||||||
|
summary.record_imported();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_note_sync_target(
|
||||||
|
&self,
|
||||||
|
target: NoteTargetSyncRecord,
|
||||||
|
profile_id: &ProfileId,
|
||||||
|
space_id: &SpaceId,
|
||||||
|
source_url: &UrlText,
|
||||||
|
) -> Result<NoteTarget, SyncClientError> {
|
||||||
|
match target {
|
||||||
|
NoteTargetSyncRecord::Url { url } => {
|
||||||
|
UrlText::parse(&url).map(NoteTarget::Url).map_err(snapshot_schema_error)
|
||||||
|
}
|
||||||
|
NoteTargetSyncRecord::Tab { tab_id } => {
|
||||||
|
let remote_tab_id = TabId::parse(tab_id).map_err(snapshot_schema_error)?;
|
||||||
|
if let Some(tab) = self.tabs.iter().find(|tab| tab.id() == &remote_tab_id) {
|
||||||
|
return Ok(NoteTarget::Tab(tab.id().clone()));
|
||||||
|
}
|
||||||
|
if let Some(tab) = self.tabs.iter().find(|tab| {
|
||||||
|
tab.profile_id() == profile_id
|
||||||
|
&& tab.space_id() == space_id
|
||||||
|
&& tab.url() == source_url
|
||||||
|
}) {
|
||||||
|
return Ok(NoteTarget::Tab(tab.id().clone()));
|
||||||
|
}
|
||||||
|
Ok(NoteTarget::Url(source_url.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_sync_index(
|
||||||
|
&self,
|
||||||
|
note_id: &NoteId,
|
||||||
|
profile_id: &ProfileId,
|
||||||
|
target: &NoteTarget,
|
||||||
|
) -> Option<usize> {
|
||||||
|
self.notes.iter().position(|note| note.id() == note_id).or_else(|| {
|
||||||
|
self.notes.iter().position(|note| {
|
||||||
|
note.profile_id() == profile_id
|
||||||
|
&& note_targets_match_for_sync(note.target(), target)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_targets_match_for_sync(left: &NoteTarget, right: &NoteTarget) -> bool {
|
||||||
|
match (left, right) {
|
||||||
|
(NoteTarget::Url(left_url), NoteTarget::Url(right_url)) => left_url == right_url,
|
||||||
|
(NoteTarget::Tab(left_tab), NoteTarget::Tab(right_tab)) => left_tab == right_tab,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use ely_domain::{ArchivePolicy, BookmarkEntry, BrowserTab, Space, TabFlags};
|
use ely_domain::{
|
||||||
|
ArchivePolicy, BookmarkEntry, BrowserTab, NoteEntry, NoteTarget, Space, TabFlags,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::state::BrowserCore;
|
use crate::state::BrowserCore;
|
||||||
@@ -15,6 +17,8 @@ pub(crate) struct SyncSnapshotBody {
|
|||||||
pub(crate) bookmarks: Vec<BookmarkSyncRecord>,
|
pub(crate) bookmarks: Vec<BookmarkSyncRecord>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) tabs: Vec<TabSyncRecord>,
|
pub(crate) tabs: Vec<TabSyncRecord>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) notes: Vec<NoteSyncRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncSnapshotBody {
|
impl SyncSnapshotBody {
|
||||||
@@ -43,6 +47,13 @@ impl SyncSnapshotBody {
|
|||||||
TabSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
TabSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
notes: core
|
||||||
|
.visible_notes_for_sync()
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| {
|
||||||
|
NoteSyncRecord::from_entry(entry, core.sync_space_name_for(entry.space_id()))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +186,54 @@ impl TabSyncRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub(crate) struct NoteSyncRecord {
|
||||||
|
pub(crate) id: String,
|
||||||
|
pub(crate) profile_id: String,
|
||||||
|
pub(crate) space_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub(crate) space_name: Option<String>,
|
||||||
|
pub(crate) target: NoteTargetSyncRecord,
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) source_url: String,
|
||||||
|
pub(crate) body: String,
|
||||||
|
pub(crate) created_at_secs: u64,
|
||||||
|
pub(crate) updated_at_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteSyncRecord {
|
||||||
|
fn from_entry(entry: &NoteEntry, space_name: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: entry.id().as_str().to_string(),
|
||||||
|
profile_id: entry.profile_id().as_str().to_string(),
|
||||||
|
space_id: entry.space_id().as_str().to_string(),
|
||||||
|
space_name,
|
||||||
|
target: NoteTargetSyncRecord::from_note_target(entry.target()),
|
||||||
|
title: entry.title().to_string(),
|
||||||
|
source_url: entry.source_url().as_str().to_string(),
|
||||||
|
body: entry.body().to_string(),
|
||||||
|
created_at_secs: system_time_secs(entry.created_at()),
|
||||||
|
updated_at_secs: system_time_secs(entry.updated_at()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub(crate) enum NoteTargetSyncRecord {
|
||||||
|
Url { url: String },
|
||||||
|
Tab { tab_id: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteTargetSyncRecord {
|
||||||
|
fn from_note_target(target: &NoteTarget) -> Self {
|
||||||
|
match target {
|
||||||
|
NoteTarget::Url(url) => Self::Url { url: url.as_str().to_string() },
|
||||||
|
NoteTarget::Tab(tab_id) => Self::Tab { tab_id: tab_id.as_str().to_string() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn default_sync_enabled() -> bool {
|
fn default_sync_enabled() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::error::Error;
|
|||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
ArchivePolicy, SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState,
|
ArchivePolicy, NoteTarget, SyncConnectionState, SyncObjectKind, SyncObjectPolicy,
|
||||||
SyncObjectStatus, UrlText,
|
SyncObjectState, SyncObjectStatus, UrlText,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -292,6 +292,122 @@ fn sync_snapshot_omits_paused_bookmarks() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_snapshot_imports_remote_url_notes_into_active_scope() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let source_home_tab_id = source.snapshot()?.active_tab_id;
|
||||||
|
source.set_tab_sync_enabled(&source_home_tab_id, false)?;
|
||||||
|
let source_tab_id = source.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
source.set_tab_sync_enabled(&source_tab_id, false)?;
|
||||||
|
source.set_tab_title(&source_tab_id, "Research Brief")?;
|
||||||
|
source.save_active_url_note(" # Finding\r\n- one ")?;
|
||||||
|
let bytes = source.build_sync_snapshot_bytes()?;
|
||||||
|
|
||||||
|
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let target_profile_id = target.snapshot()?.active_profile_id;
|
||||||
|
let target_space_id = target.snapshot()?.active_space_id;
|
||||||
|
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
|
||||||
|
let snapshot = target.snapshot()?;
|
||||||
|
let [note] = snapshot.notes.as_slice() else {
|
||||||
|
return Err(format!("expected 1 note, got {}", snapshot.notes.len()).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(summary.imported(), 1);
|
||||||
|
assert_eq!(summary.updated(), 0);
|
||||||
|
assert_eq!(summary.skipped(), 0);
|
||||||
|
assert_eq!(note.profile_id(), &target_profile_id);
|
||||||
|
assert_eq!(note.space_id(), &target_space_id);
|
||||||
|
assert_eq!(note.title(), "Research Brief");
|
||||||
|
assert_eq!(note.body(), "# Finding\n- one");
|
||||||
|
assert_eq!(note.source_url().as_str(), "https://example.com/research");
|
||||||
|
assert_eq!(note.target(), &NoteTarget::Url(UrlText::parse("https://example.com/research")?));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_snapshot_imports_remote_tab_notes_after_tabs() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let source_home_tab_id = source.snapshot()?.active_tab_id;
|
||||||
|
source.set_tab_sync_enabled(&source_home_tab_id, false)?;
|
||||||
|
let source_tab_id = source.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
source.set_tab_title(&source_tab_id, "Research Brief")?;
|
||||||
|
source.save_active_tab_note("pinned tab context")?;
|
||||||
|
let bytes = source.build_sync_snapshot_bytes()?;
|
||||||
|
|
||||||
|
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
|
||||||
|
let snapshot = target.snapshot()?;
|
||||||
|
let imported_tab = snapshot
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| tab.url().as_str() == "https://example.com/research")
|
||||||
|
.ok_or("missing imported tab")?;
|
||||||
|
let imported_note = snapshot
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.find(|note| note.source_url().as_str() == "https://example.com/research")
|
||||||
|
.ok_or("missing imported note")?;
|
||||||
|
|
||||||
|
assert_eq!(summary.imported(), 2);
|
||||||
|
assert_eq!(summary.updated(), 0);
|
||||||
|
assert_eq!(summary.skipped(), 0);
|
||||||
|
assert_eq!(imported_note.target(), &NoteTarget::Tab(imported_tab.id().clone()));
|
||||||
|
assert_eq!(imported_note.body(), "pinned tab context");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_snapshot_updates_existing_note_body() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let source_home_tab_id = source.snapshot()?.active_tab_id;
|
||||||
|
source.set_tab_sync_enabled(&source_home_tab_id, false)?;
|
||||||
|
let source_tab_id = source.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
source.set_tab_sync_enabled(&source_tab_id, false)?;
|
||||||
|
source.set_tab_title(&source_tab_id, "Research Brief")?;
|
||||||
|
source.save_active_url_note("canonical note")?;
|
||||||
|
let bytes = source.build_sync_snapshot_bytes()?;
|
||||||
|
|
||||||
|
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let target_tab_id = target.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
target.set_tab_title(&target_tab_id, "Old Title")?;
|
||||||
|
let target_note_id = target.save_active_url_note("old note")?;
|
||||||
|
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
|
||||||
|
let snapshot = target.snapshot()?;
|
||||||
|
let [note] = snapshot.notes.as_slice() else {
|
||||||
|
return Err(format!("expected 1 note, got {}", snapshot.notes.len()).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(summary.imported(), 0);
|
||||||
|
assert_eq!(summary.updated(), 1);
|
||||||
|
assert_eq!(summary.skipped(), 0);
|
||||||
|
assert_eq!(note.id(), &target_note_id);
|
||||||
|
assert_eq!(note.title(), "Research Brief");
|
||||||
|
assert_eq!(note.body(), "canonical note");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_snapshot_omits_paused_notes() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut source = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let source_home_tab_id = source.snapshot()?.active_tab_id;
|
||||||
|
source.set_tab_sync_enabled(&source_home_tab_id, false)?;
|
||||||
|
let source_tab_id = source.open_tab(UrlText::parse("https://example.com/research")?);
|
||||||
|
source.set_tab_sync_enabled(&source_tab_id, false)?;
|
||||||
|
source.save_active_url_note("local only")?;
|
||||||
|
source.set_sync_object_policy(SyncObjectKind::Notes, SyncObjectPolicy::Paused);
|
||||||
|
let bytes = source.build_sync_snapshot_bytes()?;
|
||||||
|
|
||||||
|
let mut target = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let summary = target.apply_sync_snapshot_bytes(&bytes)?;
|
||||||
|
let snapshot = target.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(summary.imported(), 0);
|
||||||
|
assert_eq!(summary.updated(), 0);
|
||||||
|
assert_eq!(summary.skipped(), 0);
|
||||||
|
assert!(snapshot.notes.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_snapshot_rejects_unknown_schema_rev() -> Result<(), Box<dyn Error>> {
|
fn sync_snapshot_rejects_unknown_schema_rev() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ impl NoteEntry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn restore(id: NoteId, mut entry: Self, updated_at: SystemTime) -> Self {
|
||||||
|
entry.id = id;
|
||||||
|
entry.updated_at = updated_at;
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> &NoteId {
|
pub fn id(&self) -> &NoteId {
|
||||||
&self.id
|
&self.id
|
||||||
@@ -180,6 +187,35 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn restores_existing_note_identity_and_timestamps() -> Result<(), DomainError> {
|
||||||
|
let id = crate::NoteId::new();
|
||||||
|
let profile_id = ProfileId::new();
|
||||||
|
let space_id = SpaceId::new();
|
||||||
|
let created_at = SystemTime::UNIX_EPOCH + Duration::from_secs(10);
|
||||||
|
let updated_at = created_at + Duration::from_secs(20);
|
||||||
|
let source_url = UrlText::parse("https://example.com/restored")?;
|
||||||
|
let note = NoteEntry::new(
|
||||||
|
profile_id.clone(),
|
||||||
|
space_id.clone(),
|
||||||
|
NoteTarget::Url(source_url.clone()),
|
||||||
|
" Restored ",
|
||||||
|
source_url,
|
||||||
|
" body\r\ncopy ",
|
||||||
|
created_at,
|
||||||
|
)?;
|
||||||
|
let note = NoteEntry::restore(id.clone(), note, updated_at);
|
||||||
|
|
||||||
|
assert_eq!(note.id(), &id);
|
||||||
|
assert_eq!(note.profile_id(), &profile_id);
|
||||||
|
assert_eq!(note.space_id(), &space_id);
|
||||||
|
assert_eq!(note.title(), "Restored");
|
||||||
|
assert_eq!(note.body(), "body\ncopy");
|
||||||
|
assert_eq!(note.created_at(), created_at);
|
||||||
|
assert_eq!(note.updated_at(), updated_at);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rejects_empty_body() -> Result<(), DomainError> {
|
fn rejects_empty_body() -> Result<(), DomainError> {
|
||||||
let source_url = UrlText::parse("https://example.com")?;
|
let source_url = UrlText::parse("https://example.com")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user