use super::store::TaskStore;
use super::types::{NewTask, Task, TaskError, TaskId, TaskStatus, TaskUpdate};
use async_trait::async_trait;
#[cfg(target_arch = "wasm32")]
use crate::tokio::sync::RwLock;
#[cfg(not(target_arch = "wasm32"))]
use tokio::sync::RwLock;
pub struct MemoryTaskStore {
tasks: RwLock<Vec<Task>>,
}
impl MemoryTaskStore {
pub fn new() -> Self {
Self {
tasks: RwLock::new(Vec::new()),
}
}
pub fn with_tasks(tasks: Vec<Task>) -> Self {
Self {
tasks: RwLock::new(tasks),
}
}
pub fn len(&self) -> usize {
self.tasks.try_read().ok().map_or(0, |t| t.len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for MemoryTaskStore {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl TaskStore for MemoryTaskStore {
async fn list(&self) -> Result<Vec<Task>, TaskError> {
let tasks = self.tasks.read().await;
Ok(tasks.clone())
}
async fn get(&self, id: &TaskId) -> Result<Option<Task>, TaskError> {
let tasks = self.tasks.read().await;
Ok(tasks.iter().find(|t| &t.id == id).cloned())
}
async fn create(&self, new_task: NewTask, session_id: Option<&str>) -> Result<Task, TaskError> {
let now = chrono::Utc::now().to_rfc3339();
let task = Task {
id: TaskId::new(),
subject: new_task.subject,
description: new_task.description,
status: TaskStatus::default(),
priority: new_task.priority.unwrap_or_default(),
labels: new_task.labels.unwrap_or_default(),
blocks: new_task.blocks.unwrap_or_default(),
created_at: now.clone(),
updated_at: now,
created_by_session: session_id.map(String::from),
updated_by_session: session_id.map(String::from),
owner: new_task.owner,
metadata: new_task.metadata.unwrap_or_default(),
blocked_by: new_task.blocked_by.unwrap_or_default(),
};
let mut tasks = self.tasks.write().await;
tasks.push(task.clone());
Ok(task)
}
async fn update(
&self,
id: &TaskId,
update: TaskUpdate,
session_id: Option<&str>,
) -> Result<Task, TaskError> {
let mut tasks = self.tasks.write().await;
let task = tasks
.iter_mut()
.find(|t| &t.id == id)
.ok_or_else(|| TaskError::NotFound(id.0.clone()))?;
if let Some(subject) = update.subject {
task.subject = subject;
}
if let Some(description) = update.description {
task.description = description;
}
if let Some(status) = update.status {
task.status = status;
}
if let Some(priority) = update.priority {
task.priority = priority;
}
if let Some(labels) = update.labels {
task.labels = labels;
}
if let Some(add_blocks) = update.add_blocks {
for block_id in add_blocks {
if !task.blocks.contains(&block_id) {
task.blocks.push(block_id);
}
}
}
if let Some(remove_blocks) = update.remove_blocks {
task.blocks.retain(|b| !remove_blocks.contains(b));
}
if let Some(owner) = update.owner {
task.owner = Some(owner);
}
if let Some(metadata) = update.metadata {
for (key, value) in metadata {
if value.is_null() {
task.metadata.remove(&key);
} else {
task.metadata.insert(key, value);
}
}
}
if let Some(add_blocked_by) = update.add_blocked_by {
for block_id in add_blocked_by {
if !task.blocked_by.contains(&block_id) {
task.blocked_by.push(block_id);
}
}
}
if let Some(remove_blocked_by) = update.remove_blocked_by {
task.blocked_by.retain(|b| !remove_blocked_by.contains(b));
}
task.updated_at = chrono::Utc::now().to_rfc3339();
task.updated_by_session = session_id.map(String::from);
Ok(task.clone())
}
async fn delete(&self, id: &TaskId) -> Result<(), TaskError> {
let mut tasks = self.tasks.write().await;
let len_before = tasks.len();
tasks.retain(|t| &t.id != id);
if tasks.len() == len_before {
return Err(TaskError::NotFound(id.0.clone()));
}
Ok(())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::builtin::types::{TaskPriority, TaskStatus};
#[tokio::test]
async fn test_memory_store_create_and_get() {
let store = MemoryTaskStore::new();
let new_task = NewTask {
subject: "Test task".to_string(),
description: "Test description".to_string(),
priority: Some(TaskPriority::High),
labels: Some(vec!["test".to_string(), "important".to_string()]),
blocks: None,
..Default::default()
};
let created = store.create(new_task, Some("session-1")).await.unwrap();
assert_eq!(created.subject, "Test task");
assert_eq!(created.description, "Test description");
assert_eq!(created.priority, TaskPriority::High);
assert_eq!(
created.labels,
vec!["test".to_string(), "important".to_string()]
);
assert_eq!(created.status, TaskStatus::Pending);
assert_eq!(created.created_by_session, Some("session-1".to_string()));
assert_eq!(created.updated_by_session, Some("session-1".to_string()));
assert!(!created.created_at.is_empty());
assert!(!created.updated_at.is_empty());
assert_eq!(created.id.0.len(), 36);
let fetched = store.get(&created.id).await.unwrap();
assert!(fetched.is_some());
let fetched = fetched.unwrap();
assert_eq!(fetched.id, created.id);
assert_eq!(fetched.subject, created.subject);
assert_eq!(fetched.description, created.description);
}
#[tokio::test]
async fn test_memory_store_create_with_defaults() {
let store = MemoryTaskStore::new();
let new_task = NewTask {
subject: "Simple task".to_string(),
description: "No optional fields".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
};
let created = store.create(new_task, None).await.unwrap();
assert_eq!(created.priority, TaskPriority::Medium);
assert!(created.labels.is_empty());
assert!(created.blocks.is_empty());
assert!(created.created_by_session.is_none());
assert!(created.updated_by_session.is_none());
}
#[tokio::test]
async fn test_memory_store_get_nonexistent() {
let store = MemoryTaskStore::new();
let result = store
.get(&TaskId::from_string("nonexistent"))
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_memory_store_list() {
let store = MemoryTaskStore::new();
let tasks = store.list().await.unwrap();
assert!(tasks.is_empty());
let task1 = NewTask {
subject: "Task 1".to_string(),
description: "First task".to_string(),
priority: Some(TaskPriority::Low),
labels: None,
blocks: None,
..Default::default()
};
let task2 = NewTask {
subject: "Task 2".to_string(),
description: "Second task".to_string(),
priority: Some(TaskPriority::High),
labels: None,
blocks: None,
..Default::default()
};
let task3 = NewTask {
subject: "Task 3".to_string(),
description: "Third task".to_string(),
priority: None,
labels: Some(vec!["urgent".to_string()]),
blocks: None,
..Default::default()
};
let created1 = store.create(task1, None).await.unwrap();
let created2 = store.create(task2, None).await.unwrap();
let created3 = store.create(task3, None).await.unwrap();
let tasks = store.list().await.unwrap();
assert_eq!(tasks.len(), 3);
let ids: Vec<_> = tasks.iter().map(|t| &t.id).collect();
assert!(ids.contains(&&created1.id));
assert!(ids.contains(&&created2.id));
assert!(ids.contains(&&created3.id));
}
#[tokio::test]
async fn test_memory_store_update() {
let store = MemoryTaskStore::new();
let new_task = NewTask {
subject: "Original subject".to_string(),
description: "Original description".to_string(),
priority: Some(TaskPriority::Low),
labels: Some(vec!["initial".to_string()]),
blocks: None,
..Default::default()
};
let created = store.create(new_task, Some("session-1")).await.unwrap();
let original_created_at = created.created_at.clone();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let update = TaskUpdate {
subject: Some("Updated subject".to_string()),
description: Some("Updated description".to_string()),
status: Some(TaskStatus::InProgress),
priority: Some(TaskPriority::High),
labels: Some(vec!["updated".to_string(), "reviewed".to_string()]),
add_blocks: None,
remove_blocks: None,
owner: None,
metadata: None,
add_blocked_by: None,
remove_blocked_by: None,
};
let updated = store
.update(&created.id, update, Some("session-2"))
.await
.unwrap();
assert_eq!(updated.id, created.id);
assert_eq!(updated.subject, "Updated subject");
assert_eq!(updated.description, "Updated description");
assert_eq!(updated.status, TaskStatus::InProgress);
assert_eq!(updated.priority, TaskPriority::High);
assert_eq!(
updated.labels,
vec!["updated".to_string(), "reviewed".to_string()]
);
assert_eq!(updated.created_at, original_created_at); assert_eq!(updated.created_by_session, Some("session-1".to_string())); assert_eq!(updated.updated_by_session, Some("session-2".to_string()));
assert_ne!(updated.updated_at, original_created_at); }
#[tokio::test]
async fn test_memory_store_partial_update() {
let store = MemoryTaskStore::new();
let new_task = NewTask {
subject: "Original".to_string(),
description: "Original desc".to_string(),
priority: Some(TaskPriority::High),
labels: Some(vec!["keep".to_string()]),
blocks: None,
..Default::default()
};
let created = store.create(new_task, None).await.unwrap();
let update = TaskUpdate {
subject: Some("New subject".to_string()),
..Default::default()
};
let updated = store.update(&created.id, update, None).await.unwrap();
assert_eq!(updated.subject, "New subject");
assert_eq!(updated.description, "Original desc");
assert_eq!(updated.priority, TaskPriority::High);
assert_eq!(updated.labels, vec!["keep".to_string()]);
assert_eq!(updated.status, TaskStatus::Pending);
}
#[tokio::test]
async fn test_memory_store_update_not_found() {
let store = MemoryTaskStore::new();
let update = TaskUpdate {
subject: Some("Updated".to_string()),
..Default::default()
};
let result = store
.update(&TaskId::from_string("nonexistent"), update, None)
.await;
assert!(matches!(result, Err(TaskError::NotFound(id)) if id == "nonexistent"));
}
#[tokio::test]
async fn test_memory_store_delete() {
let store = MemoryTaskStore::new();
let new_task = NewTask {
subject: "To delete".to_string(),
description: "Will be deleted".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
};
let created = store.create(new_task, None).await.unwrap();
assert!(store.get(&created.id).await.unwrap().is_some());
assert_eq!(store.len(), 1);
store.delete(&created.id).await.unwrap();
assert!(store.get(&created.id).await.unwrap().is_none());
assert_eq!(store.len(), 0);
assert!(store.is_empty());
}
#[tokio::test]
async fn test_memory_store_delete_not_found() {
let store = MemoryTaskStore::new();
let result = store.delete(&TaskId::from_string("nonexistent")).await;
assert!(matches!(result, Err(TaskError::NotFound(id)) if id == "nonexistent"));
}
#[tokio::test]
async fn test_memory_store_delete_multiple() {
let store = MemoryTaskStore::new();
for i in 1..=5 {
store
.create(
NewTask {
subject: format!("Task {i}"),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
}
assert_eq!(store.len(), 5);
let tasks = store.list().await.unwrap();
store.delete(&tasks[1].id).await.unwrap();
store.delete(&tasks[3].id).await.unwrap();
assert_eq!(store.len(), 3);
assert!(store.get(&tasks[0].id).await.unwrap().is_some());
assert!(store.get(&tasks[1].id).await.unwrap().is_none());
assert!(store.get(&tasks[2].id).await.unwrap().is_some());
assert!(store.get(&tasks[3].id).await.unwrap().is_none());
assert!(store.get(&tasks[4].id).await.unwrap().is_some());
}
#[tokio::test]
async fn test_memory_store_add_blocks() {
let store = MemoryTaskStore::new();
let task1 = store
.create(
NewTask {
subject: "Task 1".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let task2 = store
.create(
NewTask {
subject: "Task 2".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let task3 = store
.create(
NewTask {
subject: "Task 3".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let update = TaskUpdate {
add_blocks: Some(vec![task2.id.clone(), task3.id.clone()]),
..Default::default()
};
let updated = store.update(&task1.id, update, None).await.unwrap();
assert_eq!(updated.blocks.len(), 2);
assert!(updated.blocks.contains(&task2.id));
assert!(updated.blocks.contains(&task3.id));
let update = TaskUpdate {
add_blocks: Some(vec![task2.id.clone()]),
..Default::default()
};
let updated = store.update(&task1.id, update, None).await.unwrap();
assert_eq!(updated.blocks.len(), 2); }
#[tokio::test]
async fn test_memory_store_remove_blocks() {
let store = MemoryTaskStore::new();
let blocker1 = TaskId::from_string("blocker-1");
let blocker2 = TaskId::from_string("blocker-2");
let blocker3 = TaskId::from_string("blocker-3");
let task = store
.create(
NewTask {
subject: "Main task".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: Some(vec![blocker1.clone(), blocker2.clone(), blocker3.clone()]),
..Default::default()
},
None,
)
.await
.unwrap();
assert_eq!(task.blocks.len(), 3);
let update = TaskUpdate {
remove_blocks: Some(vec![blocker2.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocks.len(), 2);
assert!(updated.blocks.contains(&blocker1));
assert!(!updated.blocks.contains(&blocker2));
assert!(updated.blocks.contains(&blocker3));
let update = TaskUpdate {
remove_blocks: Some(vec![blocker1.clone(), blocker3.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert!(updated.blocks.is_empty());
}
#[tokio::test]
async fn test_memory_store_add_and_remove_blocks_same_update() {
let store = MemoryTaskStore::new();
let blocker1 = TaskId::from_string("blocker-1");
let blocker2 = TaskId::from_string("blocker-2");
let task = store
.create(
NewTask {
subject: "Task".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: Some(vec![blocker1.clone()]),
..Default::default()
},
None,
)
.await
.unwrap();
let update = TaskUpdate {
add_blocks: Some(vec![blocker2.clone()]),
remove_blocks: Some(vec![blocker1.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocks.len(), 1);
assert!(!updated.blocks.contains(&blocker1));
assert!(updated.blocks.contains(&blocker2));
}
#[tokio::test]
async fn test_memory_store_with_tasks() {
let existing_tasks = vec![
Task {
id: TaskId::from_string("task-1"),
subject: "Existing task 1".to_string(),
description: "".to_string(),
status: TaskStatus::Completed,
priority: TaskPriority::High,
labels: vec![],
blocks: vec![],
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
created_at: "2025-01-01T00:00:00Z".to_string(),
updated_at: "2025-01-01T00:00:00Z".to_string(),
created_by_session: None,
updated_by_session: None,
},
Task {
id: TaskId::from_string("task-2"),
subject: "Existing task 2".to_string(),
description: "".to_string(),
status: TaskStatus::InProgress,
priority: TaskPriority::Medium,
labels: vec!["work".to_string()],
blocks: vec![],
owner: None,
metadata: std::collections::HashMap::new(),
blocked_by: vec![],
created_at: "2025-01-02T00:00:00Z".to_string(),
updated_at: "2025-01-02T00:00:00Z".to_string(),
created_by_session: Some("old-session".to_string()),
updated_by_session: Some("old-session".to_string()),
},
];
let store = MemoryTaskStore::with_tasks(existing_tasks);
assert_eq!(store.len(), 2);
assert!(!store.is_empty());
let task1 = store
.get(&TaskId::from_string("task-1"))
.await
.unwrap()
.unwrap();
assert_eq!(task1.subject, "Existing task 1");
assert_eq!(task1.status, TaskStatus::Completed);
let task2 = store
.get(&TaskId::from_string("task-2"))
.await
.unwrap()
.unwrap();
assert_eq!(task2.subject, "Existing task 2");
assert_eq!(task2.labels, vec!["work".to_string()]);
}
#[tokio::test]
async fn test_memory_store_default() {
let store = MemoryTaskStore::default();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[tokio::test]
async fn test_memory_store_update_owner() {
let store = MemoryTaskStore::new();
let task = store
.create(
NewTask {
subject: "Task with no owner".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
assert!(task.owner.is_none());
let update = TaskUpdate {
owner: Some("alice".to_string()),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.owner, Some("alice".to_string()));
let update = TaskUpdate {
owner: Some("bob".to_string()),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.owner, Some("bob".to_string()));
}
#[tokio::test]
async fn test_memory_store_update_metadata_merge() {
let store = MemoryTaskStore::new();
let task = store
.create(
NewTask {
subject: "Task with metadata".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
assert!(task.metadata.is_empty());
let mut metadata = std::collections::HashMap::new();
metadata.insert("key1".to_string(), serde_json::json!("value1"));
metadata.insert("key2".to_string(), serde_json::json!(100));
let update = TaskUpdate {
metadata: Some(metadata),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.metadata.len(), 2);
assert_eq!(
updated.metadata.get("key1"),
Some(&serde_json::json!("value1"))
);
assert_eq!(updated.metadata.get("key2"), Some(&serde_json::json!(100)));
let mut metadata2 = std::collections::HashMap::new();
metadata2.insert("key2".to_string(), serde_json::json!(200)); metadata2.insert("key3".to_string(), serde_json::json!("new"));
let update = TaskUpdate {
metadata: Some(metadata2),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.metadata.len(), 3);
assert_eq!(
updated.metadata.get("key1"),
Some(&serde_json::json!("value1"))
); assert_eq!(updated.metadata.get("key2"), Some(&serde_json::json!(200))); assert_eq!(
updated.metadata.get("key3"),
Some(&serde_json::json!("new"))
); }
#[tokio::test]
async fn test_memory_store_update_metadata_delete_with_null() {
let store = MemoryTaskStore::new();
let task = store
.create(
NewTask {
subject: "Task with metadata to delete".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let mut metadata = std::collections::HashMap::new();
metadata.insert("keep".to_string(), serde_json::json!("keep me"));
metadata.insert(
"delete_me".to_string(),
serde_json::json!("will be deleted"),
);
let update = TaskUpdate {
metadata: Some(metadata),
..Default::default()
};
store.update(&task.id, update, None).await.unwrap();
let mut metadata_delete = std::collections::HashMap::new();
metadata_delete.insert("delete_me".to_string(), serde_json::Value::Null);
let update = TaskUpdate {
metadata: Some(metadata_delete),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.metadata.len(), 1);
assert_eq!(
updated.metadata.get("keep"),
Some(&serde_json::json!("keep me"))
);
assert!(!updated.metadata.contains_key("delete_me"));
}
#[tokio::test]
async fn test_memory_store_update_add_blocked_by() {
let store = MemoryTaskStore::new();
let blocker1 = store
.create(
NewTask {
subject: "Blocker 1".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let blocker2 = store
.create(
NewTask {
subject: "Blocker 2".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let task = store
.create(
NewTask {
subject: "Blocked task".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
assert!(task.blocked_by.is_empty());
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker1.id.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocked_by.len(), 1);
assert!(updated.blocked_by.contains(&blocker1.id));
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker2.id.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocked_by.len(), 2);
assert!(updated.blocked_by.contains(&blocker1.id));
assert!(updated.blocked_by.contains(&blocker2.id));
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker1.id.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocked_by.len(), 2); }
#[tokio::test]
async fn test_memory_store_update_remove_blocked_by() {
let store = MemoryTaskStore::new();
let blocker1 = TaskId::from_string("blocker-1");
let blocker2 = TaskId::from_string("blocker-2");
let blocker3 = TaskId::from_string("blocker-3");
let task = store
.create(
NewTask {
subject: "Task blocked by multiple".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker1.clone(), blocker2.clone(), blocker3.clone()]),
..Default::default()
};
let task = store.update(&task.id, update, None).await.unwrap();
assert_eq!(task.blocked_by.len(), 3);
let update = TaskUpdate {
remove_blocked_by: Some(vec![blocker2.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocked_by.len(), 2);
assert!(updated.blocked_by.contains(&blocker1));
assert!(!updated.blocked_by.contains(&blocker2));
assert!(updated.blocked_by.contains(&blocker3));
let update = TaskUpdate {
remove_blocked_by: Some(vec![blocker1.clone(), blocker3.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert!(updated.blocked_by.is_empty());
}
#[tokio::test]
async fn test_memory_store_update_add_and_remove_blocked_by_same_update() {
let store = MemoryTaskStore::new();
let blocker1 = TaskId::from_string("blocker-1");
let blocker2 = TaskId::from_string("blocker-2");
let task = store
.create(
NewTask {
subject: "Task".to_string(),
description: "".to_string(),
priority: None,
labels: None,
blocks: None,
..Default::default()
},
None,
)
.await
.unwrap();
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker1.clone()]),
..Default::default()
};
store.update(&task.id, update, None).await.unwrap();
let update = TaskUpdate {
add_blocked_by: Some(vec![blocker2.clone()]),
remove_blocked_by: Some(vec![blocker1.clone()]),
..Default::default()
};
let updated = store.update(&task.id, update, None).await.unwrap();
assert_eq!(updated.blocked_by.len(), 1);
assert!(!updated.blocked_by.contains(&blocker1));
assert!(updated.blocked_by.contains(&blocker2));
}
}