Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I154fe8dc67c56fd21a734d1f984760bf6a6a6964
222 lines
7.8 KiB
Rust
222 lines
7.8 KiB
Rust
use pinakes_core::links::extract_links;
|
|
use pinakes_core::model::*;
|
|
use pinakes_core::storage::StorageBackend;
|
|
|
|
mod common;
|
|
|
|
/// Create test markdown content with multiple links
|
|
fn create_test_note_content(num_links: usize) -> String {
|
|
let mut content = String::from("# Test Note\n\n");
|
|
for i in 0..num_links {
|
|
content.push_str(&format!("Link {}: [[note_{}]]\n", i, i));
|
|
}
|
|
content
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_save_links_atomicity_success_case() {
|
|
// Setup: Create in-memory database
|
|
let storage = common::setup().await;
|
|
|
|
// Create a test note
|
|
let note_id = MediaId::new();
|
|
let item = common::make_test_markdown_item(note_id);
|
|
storage.insert_media(&item).await.unwrap();
|
|
|
|
// Extract links from test content
|
|
let content = create_test_note_content(5);
|
|
let links = extract_links(note_id, &content);
|
|
|
|
assert_eq!(links.len(), 5, "Should extract 5 links");
|
|
|
|
// Save links (first time - should succeed)
|
|
storage.save_markdown_links(note_id, &links).await.unwrap();
|
|
|
|
// Verify all links were saved
|
|
let saved_links = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved_links.len(), 5, "All 5 links should be saved");
|
|
|
|
// Update with new links
|
|
let new_content = create_test_note_content(3);
|
|
let new_links = extract_links(note_id, &new_content);
|
|
|
|
// Save again (should replace old links)
|
|
storage
|
|
.save_markdown_links(note_id, &new_links)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify old links were deleted and new links saved
|
|
let updated_links = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(
|
|
updated_links.len(),
|
|
3,
|
|
"Should have exactly 3 links after update"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_save_links_atomicity_with_valid_data() {
|
|
// This test verifies that the transaction commit works correctly
|
|
// by saving links multiple times and ensuring consistency
|
|
let storage = common::setup().await;
|
|
let note_id = MediaId::new();
|
|
let item = common::make_test_markdown_item(note_id);
|
|
storage.insert_media(&item).await.unwrap();
|
|
|
|
// First batch of links
|
|
let content1 = "[[note1]] and [[note2]]";
|
|
let links1 = extract_links(note_id, content1);
|
|
storage.save_markdown_links(note_id, &links1).await.unwrap();
|
|
|
|
let saved1 = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved1.len(), 2, "First save: 2 links");
|
|
|
|
// Second batch (replace)
|
|
let content2 = "[[note3]] [[note4]] [[note5]]";
|
|
let links2 = extract_links(note_id, content2);
|
|
storage.save_markdown_links(note_id, &links2).await.unwrap();
|
|
|
|
let saved2 = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved2.len(), 3, "Second save: 3 links (old ones deleted)");
|
|
|
|
// Third batch (empty)
|
|
storage.save_markdown_links(note_id, &[]).await.unwrap();
|
|
|
|
let saved3 = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved3.len(), 0, "Third save: 0 links (all deleted)");
|
|
|
|
// Fourth batch (restore some links)
|
|
let content4 = "[[final_note]]";
|
|
let links4 = extract_links(note_id, content4);
|
|
storage.save_markdown_links(note_id, &links4).await.unwrap();
|
|
|
|
let saved4 = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved4.len(), 1, "Fourth save: 1 link");
|
|
assert_eq!(saved4[0].target_path, "final_note", "Correct link target");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_save_links_idempotency() {
|
|
// Verify that saving the same links multiple times is safe
|
|
let storage = common::setup().await;
|
|
let note_id = MediaId::new();
|
|
let item = common::make_test_markdown_item(note_id);
|
|
storage.insert_media(&item).await.unwrap();
|
|
|
|
let content = "[[note_a]] [[note_b]]";
|
|
let links = extract_links(note_id, content);
|
|
|
|
// Save same links 3 times
|
|
storage.save_markdown_links(note_id, &links).await.unwrap();
|
|
storage.save_markdown_links(note_id, &links).await.unwrap();
|
|
storage.save_markdown_links(note_id, &links).await.unwrap();
|
|
|
|
// Should still have exactly 2 links (not duplicated)
|
|
let saved = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(
|
|
saved.len(),
|
|
2,
|
|
"Should have exactly 2 links (no duplicates)"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_save_links_concurrent_updates() {
|
|
// Test that concurrent updates to different notes don't interfere
|
|
let storage = common::setup().await;
|
|
|
|
// Create two different notes
|
|
let note1_id = MediaId::new();
|
|
let note2_id = MediaId::new();
|
|
let item1 = common::make_test_markdown_item(note1_id);
|
|
let item2 = common::make_test_markdown_item(note2_id);
|
|
storage.insert_media(&item1).await.unwrap();
|
|
storage.insert_media(&item2).await.unwrap();
|
|
|
|
// Save links for both notes
|
|
let links1 = extract_links(note1_id, "[[target1]]");
|
|
let links2 = extract_links(note2_id, "[[target2]] [[target3]]");
|
|
|
|
// Execute both saves. We do so in sequence since we can't test true concurrency easily
|
|
// ...or so I think. Database tests are annoying.
|
|
storage
|
|
.save_markdown_links(note1_id, &links1)
|
|
.await
|
|
.unwrap();
|
|
storage
|
|
.save_markdown_links(note2_id, &links2)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify both notes have correct links
|
|
let saved1 = storage.get_outgoing_links(note1_id).await.unwrap();
|
|
let saved2 = storage.get_outgoing_links(note2_id).await.unwrap();
|
|
|
|
assert_eq!(saved1.len(), 1, "Note 1 should have 1 link");
|
|
assert_eq!(saved2.len(), 2, "Note 2 should have 2 links");
|
|
|
|
// Update note 1 - should not affect note 2
|
|
let new_links1 = extract_links(note1_id, "[[target_new1]] [[target_new2]]");
|
|
storage
|
|
.save_markdown_links(note1_id, &new_links1)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify note 1 updated but note 2 unchanged
|
|
let updated1 = storage.get_outgoing_links(note1_id).await.unwrap();
|
|
let unchanged2 = storage.get_outgoing_links(note2_id).await.unwrap();
|
|
|
|
assert_eq!(updated1.len(), 2, "Note 1 should have 2 links after update");
|
|
assert_eq!(unchanged2.len(), 2, "Note 2 should still have 2 links");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_save_links_with_large_batch() {
|
|
// Test atomicity with a large number of links
|
|
let storage = common::setup().await;
|
|
let note_id = MediaId::new();
|
|
let item = common::make_test_markdown_item(note_id);
|
|
storage.insert_media(&item).await.unwrap();
|
|
|
|
// Create note with 100 links
|
|
let content = create_test_note_content(100);
|
|
let links = extract_links(note_id, &content);
|
|
|
|
assert_eq!(links.len(), 100, "Should extract 100 links");
|
|
|
|
// Save all 100 links
|
|
storage.save_markdown_links(note_id, &links).await.unwrap();
|
|
|
|
// Verify all saved
|
|
let saved = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(saved.len(), 100, "All 100 links should be saved atomically");
|
|
|
|
// Replace with smaller set
|
|
let small_content = create_test_note_content(10);
|
|
let small_links = extract_links(note_id, &small_content);
|
|
storage
|
|
.save_markdown_links(note_id, &small_links)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Verify replacement worked
|
|
let updated = storage.get_outgoing_links(note_id).await.unwrap();
|
|
assert_eq!(
|
|
updated.len(),
|
|
10,
|
|
"Should have exactly 10 links after replacement"
|
|
);
|
|
}
|
|
|
|
// XXX: Testing actual transaction rollback on error is difficult without
|
|
// mocking the database or injecting failures. The above tests verify that:
|
|
// 1. Normal operation is atomic (delete + insert works correctly)
|
|
// 2. Updates properly replace old links
|
|
// 3. Empty link sets work correctly
|
|
// 4. Large batches are handled atomically
|
|
// 5. Concurrent operations on different notes don't interfere
|
|
//
|
|
// The transaction wrapper ensures that if ANY operation fails during
|
|
// the DELETE + INSERT sequence, the entire operation rolls back,
|
|
// preventing partial states.
|