sentinel_dbms/
collection.rs

1use std::{path::PathBuf, sync::Arc};
2
3use serde_json::Value;
4use tokio::fs as tokio_fs;
5use tracing::{debug, error, trace, warn};
6
7use crate::{
8    validation::{is_reserved_name, is_valid_document_id_chars},
9    Document,
10    Result,
11    SentinelError,
12};
13
14/// A collection represents a namespace for documents in the Sentinel database.
15///
16/// Collections are backed by filesystem directories, where each document is stored
17/// as a JSON file. The collection provides CRUD operations (Create, Read, Update, Delete)
18/// for managing documents asynchronously using tokio.
19///
20/// # Structure
21///
22/// Each collection is stored in a directory with the following structure:
23/// - `{collection_name}/` - Root directory for the collection
24/// - `{collection_name}/{id}.json` - Individual document files
25///
26/// # Example
27///
28/// ```rust
29/// use sentinel_dbms::{Store, Collection};
30/// use serde_json::json;
31///
32/// # async fn example() -> sentinel_dbms::Result<()> {
33/// // Create a store and get a collection
34/// let store = Store::new("/path/to/data", None).await?;
35/// let collection = store.collection("users").await?;
36///
37/// // Insert a document
38/// let user_data = json!({
39///     "name": "Alice",
40///     "email": "alice@example.com"
41/// });
42/// collection.insert("user-123", user_data).await?;
43///
44/// // Retrieve the document
45/// let doc = collection.get("user-123").await?;
46/// assert!(doc.is_some());
47/// # Ok(())
48/// # }
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51#[allow(
52    clippy::field_scoped_visibility_modifiers,
53    reason = "fields need to be pub(crate) for internal access"
54)]
55pub struct Collection {
56    /// The filesystem path to the collection directory.
57    pub(crate) path:        PathBuf,
58    /// The signing key for the collection.
59    pub(crate) signing_key: Option<Arc<sentinel_crypto::SigningKey>>,
60}
61
62impl Collection {
63    /// Returns the name of the collection.
64    pub fn name(&self) -> &str { self.path.file_name().unwrap().to_str().unwrap() }
65
66    /// Inserts a new document into the collection or overwrites an existing one.
67    ///
68    /// The document is serialized to pretty-printed JSON and written to a file named
69    /// `{id}.json` within the collection's directory. If a document with the same ID
70    /// already exists, it will be overwritten.
71    ///
72    /// # Arguments
73    ///
74    /// * `id` - A unique identifier for the document. This will be used as the filename (with
75    ///   `.json` extension). Must be filesystem-safe.
76    /// * `data` - The JSON data to store. Can be any valid `serde_json::Value`.
77    ///
78    /// # Returns
79    ///
80    /// Returns `Ok(())` on success, or a `SentinelError` if the operation fails
81    /// (e.g., filesystem errors, serialization errors).
82    ///
83    /// # Example
84    ///
85    /// ```rust
86    /// use sentinel_dbms::{Store, Collection};
87    /// use serde_json::json;
88    ///
89    /// # async fn example() -> sentinel_dbms::Result<()> {
90    /// let store = Store::new("/path/to/data", None).await?;
91    /// let collection = store.collection("users").await?;
92    ///
93    /// let user = json!({
94    ///     "name": "Alice",
95    ///     "email": "alice@example.com",
96    ///     "age": 30
97    /// });
98    ///
99    /// collection.insert("user-123", user).await?;
100    /// # Ok(())
101    /// # }
102    /// ```
103    pub async fn insert(&self, id: &str, data: Value) -> Result<()> {
104        trace!("Inserting document with id: {}", id);
105        validate_document_id(id)?;
106        let file_path = self.path.join(format!("{}.json", id));
107
108        #[allow(clippy::pattern_type_mismatch, reason = "false positive")]
109        let doc = if let Some(key) = &self.signing_key {
110            debug!("Creating signed document for id: {}", id);
111            Document::new(id.to_owned(), data, key)?
112        }
113        else {
114            debug!("Creating unsigned document for id: {}", id);
115            Document::new_without_signature(id.to_owned(), data)?
116        };
117        let json = serde_json::to_string_pretty(&doc).map_err(|e| {
118            error!("Failed to serialize document {} to JSON: {}", id, e);
119            e
120        })?;
121        tokio_fs::write(&file_path, json).await.map_err(|e| {
122            error!(
123                "Failed to write document {} to file {:?}: {}",
124                id, file_path, e
125            );
126            e
127        })?;
128        debug!("Document {} inserted successfully", id);
129        Ok(())
130    }
131
132    /// Retrieves a document from the collection by its ID.
133    ///
134    /// Reads the JSON file corresponding to the given ID and deserializes it into
135    /// a `Document` struct. If the document doesn't exist, returns `None`.
136    ///
137    /// # Arguments
138    ///
139    /// * `id` - The unique identifier of the document to retrieve.
140    ///
141    /// # Returns
142    ///
143    /// Returns:
144    /// - `Ok(Some(Document))` if the document exists and was successfully read
145    /// - `Ok(None)` if the document doesn't exist (file not found)
146    /// - `Err(SentinelError)` if there was an error reading or parsing the document
147    ///
148    /// # Example
149    ///
150    /// ```rust
151    /// use sentinel_dbms::{Store, Collection};
152    /// use serde_json::json;
153    ///
154    /// # async fn example() -> sentinel_dbms::Result<()> {
155    /// let store = Store::new("/path/to/data", None).await?;
156    /// let collection = store.collection("users").await?;
157    ///
158    /// // Insert a document first
159    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
160    ///
161    /// // Retrieve the document
162    /// let doc = collection.get("user-123").await?;
163    /// assert!(doc.is_some());
164    /// assert_eq!(doc.unwrap().id(), "user-123");
165    ///
166    /// // Try to get a non-existent document
167    /// let missing = collection.get("user-999").await?;
168    /// assert!(missing.is_none());
169    /// # Ok(())
170    /// # }
171    /// ```
172    pub async fn get(&self, id: &str) -> Result<Option<Document>> {
173        trace!("Retrieving document with id: {}", id);
174        validate_document_id(id)?;
175        let file_path = self.path.join(format!("{}.json", id));
176        match tokio_fs::read_to_string(&file_path).await {
177            Ok(content) => {
178                debug!("Document {} found, parsing JSON", id);
179                let mut doc: Document = serde_json::from_str(&content).map_err(|e| {
180                    error!("Failed to parse JSON for document {}: {}", id, e);
181                    e
182                })?;
183                // Ensure the id matches the filename
184                doc.id = id.to_owned();
185                trace!("Document {} retrieved successfully", id);
186                Ok(Some(doc))
187            },
188            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
189                debug!("Document {} not found", id);
190                Ok(None)
191            },
192            Err(e) => {
193                error!("IO error reading document {}: {}", id, e);
194                Err(SentinelError::Io {
195                    source: e,
196                })
197            },
198        }
199    }
200
201    /// Updates an existing document or creates a new one if it doesn't exist.
202    ///
203    /// This method is semantically equivalent to `insert` in the current implementation,
204    /// as it overwrites the entire document. Future versions may implement partial updates
205    /// or version tracking.
206    ///
207    /// # Arguments
208    ///
209    /// * `id` - The unique identifier of the document to update.
210    /// * `data` - The new JSON data that will replace the existing document.
211    ///
212    /// # Returns
213    ///
214    /// Returns `Ok(())` on success, or a `SentinelError` if the operation fails.
215    ///
216    /// # Example
217    ///
218    /// ```rust
219    /// use sentinel_dbms::{Store, Collection};
220    /// use serde_json::json;
221    ///
222    /// # async fn example() -> sentinel_dbms::Result<()> {
223    /// let store = Store::new("/path/to/data", None).await?;
224    /// let collection = store.collection("users").await?;
225    ///
226    /// // Insert initial document
227    /// collection.insert("user-123", json!({"name": "Alice", "age": 30})).await?;
228    ///
229    /// // Update the document with new data
230    /// collection.update("user-123", json!({"name": "Alice", "age": 31})).await?;
231    ///
232    /// // Verify the update
233    /// let doc = collection.get("user-123").await?.unwrap();
234    /// assert_eq!(doc.data()["age"], 31);
235    /// # Ok(())
236    /// # }
237    /// ```
238    pub async fn update(&self, id: &str, data: Value) -> Result<()> {
239        // For update, just insert (overwrite)
240        self.insert(id, data).await
241    }
242
243    /// Deletes a document from the collection (soft delete).
244    ///
245    /// Moves the JSON file corresponding to the given ID to a `.deleted/` subdirectory
246    /// within the collection. This implements soft deletes, allowing for recovery
247    /// of accidentally deleted documents. The `.deleted/` directory is created
248    /// automatically if it doesn't exist.
249    ///
250    /// If the document doesn't exist, the operation succeeds silently (idempotent).
251    ///
252    /// # Arguments
253    ///
254    /// * `id` - The unique identifier of the document to delete.
255    ///
256    /// # Returns
257    ///
258    /// Returns `Ok(())` on success (including when the document doesn't exist),
259    /// or a `SentinelError` if the operation fails due to filesystem errors.
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use sentinel_dbms::{Store, Collection};
265    /// use serde_json::json;
266    ///
267    /// # async fn example() -> sentinel_dbms::Result<()> {
268    /// let store = Store::new("/path/to/data", None).await?;
269    /// let collection = store.collection("users").await?;
270    ///
271    /// // Insert a document
272    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
273    ///
274    /// // Soft delete the document
275    /// collection.delete("user-123").await?;
276    ///
277    /// // Document is no longer accessible via get()
278    /// let doc = collection.get("user-123").await?;
279    /// assert!(doc.is_none());
280    ///
281    /// // But the file still exists in .deleted/
282    /// // (can be recovered manually if needed)
283    /// # Ok(())
284    /// # }
285    /// ```
286    pub async fn delete(&self, id: &str) -> Result<()> {
287        trace!("Deleting document with id: {}", id);
288        validate_document_id(id)?;
289        let source_path = self.path.join(format!("{}.json", id));
290        let deleted_dir = self.path.join(".deleted");
291        let dest_path = deleted_dir.join(format!("{}.json", id));
292
293        // Check if source exists
294        match tokio_fs::metadata(&source_path).await {
295            Ok(_) => {
296                debug!("Document {} exists, moving to .deleted", id);
297                // Create .deleted directory if it doesn't exist
298                tokio_fs::create_dir_all(&deleted_dir).await.map_err(|e| {
299                    error!(
300                        "Failed to create .deleted directory {:?}: {}",
301                        deleted_dir, e
302                    );
303                    e
304                })?;
305                // Move file to .deleted/
306                tokio_fs::rename(&source_path, &dest_path)
307                    .await
308                    .map_err(|e| {
309                        error!("Failed to move document {} to .deleted: {}", id, e);
310                        e
311                    })?;
312                debug!("Document {} soft deleted successfully", id);
313                Ok(())
314            },
315            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
316                debug!(
317                    "Document {} not found, already deleted or never existed",
318                    id
319                );
320                Ok(())
321            },
322            Err(e) => {
323                error!("IO error checking document {} existence: {}", id, e);
324                Err(SentinelError::Io {
325                    source: e,
326                })
327            },
328        }
329    }
330
331    /// Lists all document IDs in the collection.
332    ///
333    /// Scans the collection directory for JSON files and returns their IDs
334    /// (filenames without the .json extension). This operation reads the directory
335    /// contents and filters for valid document files, skipping hidden directories
336    /// and metadata directories for optimization.
337    ///
338    /// # Returns
339    ///
340    /// Returns `Ok(Vec<String>)` containing all document IDs in the collection,
341    /// or a `SentinelError` if the operation fails due to filesystem errors.
342    ///
343    /// # Example
344    ///
345    /// ```rust
346    /// use sentinel_dbms::{Store, Collection};
347    /// use serde_json::json;
348    ///
349    /// # async fn example() -> sentinel_dbms::Result<()> {
350    /// let store = Store::new("/path/to/data", None).await?;
351    /// let collection = store.collection("users").await?;
352    ///
353    /// // Insert some documents
354    /// collection.insert("user-123", json!({"name": "Alice"})).await?;
355    /// collection.insert("user-456", json!({"name": "Bob"})).await?;
356    ///
357    /// // List all documents
358    /// let ids = collection.list().await?;
359    /// assert_eq!(ids.len(), 2);
360    /// assert!(ids.contains(&"user-123".to_string()));
361    /// assert!(ids.contains(&"user-456".to_string()));
362    /// # Ok(())
363    /// # }
364    /// ```
365    pub async fn list(&self) -> Result<Vec<String>> {
366        trace!("Listing documents in collection: {}", self.name());
367        let mut entries = tokio_fs::read_dir(&self.path).await.map_err(|e| {
368            error!("Failed to read collection directory {:?}: {}", self.path, e);
369            e
370        })?;
371        let mut ids = Vec::new();
372
373        while let Some(entry) = entries.next_entry().await? {
374            let path = entry.path();
375            if !entry.file_type().await?.is_dir() &&
376                let Some(extension) = path.extension() &&
377                extension == "json" &&
378                let Some(file_stem) = path.file_stem() &&
379                let Some(id) = file_stem.to_str()
380            {
381                ids.push(id.to_owned());
382            }
383            // Skip directories (optimization)
384        }
385
386        // Sort for consistent ordering
387        ids.sort();
388        debug!(
389            "Found {} documents in collection {}",
390            ids.len(),
391            self.name()
392        );
393        trace!("Documents listed successfully");
394        Ok(ids)
395    }
396
397    /// Performs bulk insert operations on multiple documents.
398    ///
399    /// Inserts multiple documents into the collection in a single operation.
400    /// If any document fails to insert, the operation stops and returns an error.
401    /// Documents are inserted in the order provided.
402    ///
403    /// # Arguments
404    ///
405    /// * `documents` - A vector of (id, data) tuples to insert.
406    ///
407    /// # Returns
408    ///
409    /// Returns `Ok(())` on success, or a `SentinelError` if any operation fails.
410    /// In case of failure, some documents may have been inserted before the error.
411    ///
412    /// # Example
413    ///
414    /// ```rust
415    /// use sentinel_dbms::{Store, Collection};
416    /// use serde_json::json;
417    ///
418    /// # async fn example() -> sentinel_dbms::Result<()> {
419    /// let store = Store::new("/path/to/data", None).await?;
420    /// let collection = store.collection("users").await?;
421    ///
422    /// // Prepare bulk documents
423    /// let documents = vec![
424    ///     ("user-123", json!({"name": "Alice", "role": "admin"})),
425    ///     ("user-456", json!({"name": "Bob", "role": "user"})),
426    ///     ("user-789", json!({"name": "Charlie", "role": "user"})),
427    /// ];
428    ///
429    /// // Bulk insert
430    /// collection.bulk_insert(documents).await?;
431    ///
432    /// // Verify all documents were inserted
433    /// assert!(collection.get("user-123").await?.is_some());
434    /// assert!(collection.get("user-456").await?.is_some());
435    /// assert!(collection.get("user-789").await?.is_some());
436    /// # Ok(())
437    /// # }
438    /// ```
439    pub async fn bulk_insert(&self, documents: Vec<(&str, Value)>) -> Result<()> {
440        let count = documents.len();
441        trace!(
442            "Bulk inserting {} documents into collection {}",
443            count,
444            self.name()
445        );
446        for (id, data) in documents {
447            self.insert(id, data).await?;
448        }
449        debug!("Bulk insert of {} documents completed successfully", count);
450        Ok(())
451    }
452}
453
454/// Validates that a document ID is filesystem-safe across all platforms.
455///
456/// # Rules
457/// - Must not be empty
458/// - Must not contain path separators (`/` or `\`)
459/// - Must not contain control characters (0x00-0x1F, 0x7F)
460/// - Must not be a Windows reserved name (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
461/// - Must not contain Windows reserved characters (< > : " | ? *)
462/// - Must only contain valid filename characters
463///
464/// # Parameters
465/// - `id`: The document ID to validate
466///
467/// # Returns
468/// - `Ok(())` if the ID is valid
469/// - `Err(SentinelError::InvalidDocumentId)` if the ID is invalid
470pub(crate) fn validate_document_id(id: &str) -> Result<()> {
471    trace!("Validating document id: {}", id);
472    // Check if id is empty
473    if id.is_empty() {
474        warn!("Document id is empty");
475        return Err(SentinelError::InvalidDocumentId {
476            id: id.to_owned(),
477        });
478    }
479
480    // Check for valid characters
481    if !is_valid_document_id_chars(id) {
482        warn!("Document id contains invalid characters: {}", id);
483        return Err(SentinelError::InvalidDocumentId {
484            id: id.to_owned(),
485        });
486    }
487
488    // Check for Windows reserved names
489    if is_reserved_name(id) {
490        warn!("Document id is a reserved name: {}", id);
491        return Err(SentinelError::InvalidDocumentId {
492            id: id.to_owned(),
493        });
494    }
495
496    trace!("Document id '{}' is valid", id);
497    Ok(())
498}
499
500#[cfg(test)]
501mod tests {
502    use serde_json::json;
503    use tempfile::tempdir;
504
505    use super::*;
506    use crate::Store;
507
508    /// Helper function to set up a temporary collection for testing
509    async fn setup_collection() -> (Collection, tempfile::TempDir) {
510        let temp_dir = tempdir().unwrap();
511        let store = Store::new(temp_dir.path(), None).await.unwrap();
512        let collection = store.collection("test_collection").await.unwrap();
513        (collection, temp_dir)
514    }
515
516    /// Helper function to set up a temporary collection with signing key for testing
517    async fn setup_collection_with_signing_key() -> (Collection, tempfile::TempDir) {
518        let temp_dir = tempdir().unwrap();
519        let store = Store::new(temp_dir.path(), Some("test_passphrase"))
520            .await
521            .unwrap();
522        let collection = store.collection("test_collection").await.unwrap();
523        (collection, temp_dir)
524    }
525
526    #[tokio::test]
527    async fn test_insert_and_retrieve() {
528        let (collection, _temp_dir) = setup_collection().await;
529
530        let doc = json!({ "name": "Alice", "email": "alice@example.com" });
531        collection.insert("user-123", doc.clone()).await.unwrap();
532
533        let retrieved = collection.get("user-123").await.unwrap();
534        assert_eq!(*retrieved.unwrap().data(), doc);
535    }
536
537    #[tokio::test]
538    async fn test_insert_empty_document() {
539        let (collection, _temp_dir) = setup_collection().await;
540
541        let doc = json!({});
542        collection.insert("empty", doc.clone()).await.unwrap();
543
544        let retrieved = collection.get("empty").await.unwrap();
545        assert_eq!(*retrieved.unwrap().data(), doc);
546    }
547
548    #[tokio::test]
549    async fn test_insert_with_signing_key() {
550        let (collection, _temp_dir) = setup_collection_with_signing_key().await;
551
552        let doc = json!({ "name": "Alice", "signed": true });
553        collection.insert("signed_doc", doc.clone()).await.unwrap();
554
555        let retrieved = collection.get("signed_doc").await.unwrap().unwrap();
556        assert_eq!(*retrieved.data(), doc);
557        // Check that signature is not empty
558        assert!(!retrieved.signature().is_empty());
559    }
560
561    #[tokio::test]
562    async fn test_insert_large_document() {
563        let (collection, _temp_dir) = setup_collection().await;
564
565        let large_data = json!({
566            "large_array": (0..1000).collect::<Vec<_>>(),
567            "nested": {
568                "deep": {
569                    "value": "test"
570                }
571            }
572        });
573        collection
574            .insert("large", large_data.clone())
575            .await
576            .unwrap();
577
578        let retrieved = collection.get("large").await.unwrap();
579        assert_eq!(*retrieved.unwrap().data(), large_data);
580    }
581
582    #[tokio::test]
583    async fn test_insert_with_invalid_special_characters_in_id() {
584        let (collection, _temp_dir) = setup_collection().await;
585
586        let doc = json!({ "data": "test" });
587        let result = collection.insert("user_123-special!", doc.clone()).await;
588
589        // Should return an error for invalid document ID with special characters
590        assert!(result.is_err());
591        match result {
592            Err(SentinelError::InvalidDocumentId {
593                id,
594            }) => {
595                assert_eq!(id, "user_123-special!");
596            },
597            _ => panic!("Expected InvalidDocumentId error"),
598        }
599    }
600
601    #[tokio::test]
602    async fn test_insert_with_valid_document_ids() {
603        let (collection, _temp_dir) = setup_collection().await;
604
605        // Test various valid document IDs
606        let valid_ids = vec![
607            "user-123",
608            "user_456",
609            "user123",
610            "123",
611            "a",
612            "user-123_test",
613            "user_123-test",
614            "CamelCaseID",
615            "lower_case_id",
616            "UPPER_CASE_ID",
617        ];
618
619        for id in valid_ids {
620            let doc = json!({ "data": "test" });
621            let result = collection.insert(id, doc).await;
622            assert!(
623                result.is_ok(),
624                "Expected ID '{}' to be valid but got error: {:?}",
625                id,
626                result
627            );
628        }
629    }
630
631    #[tokio::test]
632    async fn test_insert_with_various_invalid_document_ids() {
633        let (collection, _temp_dir) = setup_collection().await;
634
635        // Test various invalid document IDs
636        let invalid_ids = vec![
637            "user!123",    // exclamation mark
638            "user@domain", // at sign
639            "user#123",    // hash
640            "user$123",    // dollar sign
641            "user%123",    // percent
642            "user^123",    // caret
643            "user&123",    // ampersand
644            "user*123",    // asterisk
645            "user(123)",   // parentheses
646            "user.123",    // period
647            "user/123",    // forward slash
648            "user\\123",   // backslash
649            "user:123",    // colon
650            "user;123",    // semicolon
651            "user<123",    // less than
652            "user>123",    // greater than
653            "user?123",    // question mark
654            "user|123",    // pipe
655            "user\"123",   // quote
656            "user'123",    // single quote
657            "",            // empty string
658        ];
659
660        for id in invalid_ids {
661            let doc = json!({ "data": "test" });
662            let result = collection.insert(id, doc).await;
663            assert!(
664                result.is_err(),
665                "Expected ID '{}' to be invalid but insertion succeeded",
666                id
667            );
668            match result {
669                Err(SentinelError::InvalidDocumentId {
670                    ..
671                }) => {
672                    // Expected error type
673                },
674                _ => panic!("Expected InvalidDocumentId error for ID '{}'", id),
675            }
676        }
677    }
678
679    #[tokio::test]
680    async fn test_get_nonexistent() {
681        let (collection, _temp_dir) = setup_collection().await;
682
683        let retrieved = collection.get("nonexistent").await.unwrap();
684        assert!(retrieved.is_none());
685    }
686
687    #[tokio::test]
688    async fn test_update() {
689        let (collection, _temp_dir) = setup_collection().await;
690
691        let doc1 = json!({ "name": "Alice" });
692        collection.insert("user-123", doc1).await.unwrap();
693
694        let doc2 = json!({ "name": "Alice", "age": 30 });
695        collection.update("user-123", doc2.clone()).await.unwrap();
696
697        let retrieved = collection.get("user-123").await.unwrap();
698        assert_eq!(*retrieved.unwrap().data(), doc2);
699    }
700
701    #[tokio::test]
702    async fn test_update_nonexistent() {
703        let (collection, _temp_dir) = setup_collection().await;
704
705        let doc = json!({ "name": "Bob" });
706        collection.update("new-user", doc.clone()).await.unwrap();
707
708        let retrieved = collection.get("new-user").await.unwrap();
709        assert_eq!(*retrieved.unwrap().data(), doc);
710    }
711
712    #[tokio::test]
713    async fn test_update_with_invalid_id() {
714        let (collection, _temp_dir) = setup_collection().await;
715
716        let doc = json!({ "name": "Bob" });
717        let result = collection.update("user!invalid", doc).await;
718
719        // Should return an error for invalid document ID
720        assert!(result.is_err());
721        match result {
722            Err(SentinelError::InvalidDocumentId {
723                id,
724            }) => {
725                assert_eq!(id, "user!invalid");
726            },
727            _ => panic!("Expected InvalidDocumentId error"),
728        }
729    }
730
731    #[tokio::test]
732    async fn test_delete() {
733        let (collection, _temp_dir) = setup_collection().await;
734
735        let doc = json!({ "name": "Alice" });
736        collection.insert("user-123", doc).await.unwrap();
737
738        let retrieved = collection.get("user-123").await.unwrap();
739        assert!(retrieved.is_some());
740
741        collection.delete("user-123").await.unwrap();
742
743        let retrieved = collection.get("user-123").await.unwrap();
744        assert!(retrieved.is_none());
745
746        // Check that file was moved to .deleted/
747        let deleted_path = collection.path.join(".deleted").join("user-123.json");
748        assert!(tokio_fs::try_exists(&deleted_path).await.unwrap());
749    }
750
751    #[tokio::test]
752    async fn test_delete_nonexistent() {
753        let (collection, _temp_dir) = setup_collection().await;
754
755        // Should not error
756        collection.delete("nonexistent").await.unwrap();
757    }
758
759    #[tokio::test]
760    async fn test_list_empty_collection() {
761        let (collection, _temp_dir) = setup_collection().await;
762
763        let ids = collection.list().await.unwrap();
764        assert!(ids.is_empty());
765    }
766
767    #[tokio::test]
768    async fn test_list_with_documents() {
769        let (collection, _temp_dir) = setup_collection().await;
770
771        collection
772            .insert("user-123", json!({"name": "Alice"}))
773            .await
774            .unwrap();
775        collection
776            .insert("user-456", json!({"name": "Bob"}))
777            .await
778            .unwrap();
779        collection
780            .insert("user-789", json!({"name": "Charlie"}))
781            .await
782            .unwrap();
783
784        let ids = collection.list().await.unwrap();
785        assert_eq!(ids.len(), 3);
786        assert_eq!(ids, vec!["user-123", "user-456", "user-789"]);
787    }
788
789    #[tokio::test]
790    async fn test_list_skips_deleted_documents() {
791        let (collection, _temp_dir) = setup_collection().await;
792
793        collection
794            .insert("user-123", json!({"name": "Alice"}))
795            .await
796            .unwrap();
797        collection
798            .insert("user-456", json!({"name": "Bob"}))
799            .await
800            .unwrap();
801        collection.delete("user-456").await.unwrap();
802
803        let ids = collection.list().await.unwrap();
804        assert_eq!(ids.len(), 1);
805        assert_eq!(ids, vec!["user-123"]);
806    }
807
808    #[tokio::test]
809    async fn test_bulk_insert() {
810        let (collection, _temp_dir) = setup_collection().await;
811
812        let documents = vec![
813            ("user-123", json!({"name": "Alice", "role": "admin"})),
814            ("user-456", json!({"name": "Bob", "role": "user"})),
815            ("user-789", json!({"name": "Charlie", "role": "user"})),
816        ];
817
818        collection.bulk_insert(documents).await.unwrap();
819
820        let ids = collection.list().await.unwrap();
821        assert_eq!(ids.len(), 3);
822        assert!(ids.contains(&"user-123".to_string()));
823        assert!(ids.contains(&"user-456".to_string()));
824        assert!(ids.contains(&"user-789".to_string()));
825
826        // Verify data
827        let alice = collection.get("user-123").await.unwrap().unwrap();
828        assert_eq!(alice.data()["name"], "Alice");
829        assert_eq!(alice.data()["role"], "admin");
830    }
831
832    #[tokio::test]
833    async fn test_bulk_insert_empty() {
834        let (collection, _temp_dir) = setup_collection().await;
835
836        collection.bulk_insert(vec![]).await.unwrap();
837
838        let ids = collection.list().await.unwrap();
839        assert!(ids.is_empty());
840    }
841
842    #[tokio::test]
843    async fn test_bulk_insert_with_invalid_id() {
844        let (collection, _temp_dir) = setup_collection().await;
845
846        let documents = vec![
847            ("user-123", json!({"name": "Alice"})),
848            ("user!invalid", json!({"name": "Bob"})),
849        ];
850
851        let result = collection.bulk_insert(documents).await;
852        assert!(result.is_err());
853
854        // First document should have been inserted before error
855        let ids = collection.list().await.unwrap();
856        assert_eq!(ids.len(), 1);
857        assert_eq!(ids[0], "user-123");
858    }
859
860    #[tokio::test]
861    async fn test_multiple_operations() {
862        let (collection, _temp_dir) = setup_collection().await;
863
864        // Insert multiple
865        collection
866            .insert("user1", json!({"name": "User1"}))
867            .await
868            .unwrap();
869        collection
870            .insert("user2", json!({"name": "User2"}))
871            .await
872            .unwrap();
873
874        // Get both
875        let user1 = collection.get("user1").await.unwrap().unwrap();
876        let user2 = collection.get("user2").await.unwrap().unwrap();
877        assert_eq!(user1.data()["name"], "User1");
878        assert_eq!(user2.data()["name"], "User2");
879
880        // Update one
881        collection
882            .update("user1", json!({"name": "Updated"}))
883            .await
884            .unwrap();
885        let updated = collection.get("user1").await.unwrap().unwrap();
886        assert_eq!(updated.data()["name"], "Updated");
887
888        // Delete one
889        collection.delete("user2").await.unwrap();
890        assert!(collection.get("user2").await.unwrap().is_none());
891        assert!(collection.get("user1").await.unwrap().is_some());
892    }
893
894    #[test]
895    fn test_validate_document_id_valid() {
896        // Valid IDs
897        assert!(validate_document_id("user-123").is_ok());
898        assert!(validate_document_id("user_456").is_ok());
899        assert!(validate_document_id("data-item").is_ok());
900        assert!(validate_document_id("test_collection_123").is_ok());
901        assert!(validate_document_id("file-txt").is_ok());
902        assert!(validate_document_id("a").is_ok());
903        assert!(validate_document_id("123").is_ok());
904    }
905
906    #[test]
907    fn test_validate_document_id_invalid_empty() {
908        assert!(validate_document_id("").is_err());
909    }
910
911    #[test]
912    fn test_validate_document_id_invalid_path_separators() {
913        assert!(validate_document_id("path/traversal").is_err());
914        assert!(validate_document_id("path\\traversal").is_err());
915    }
916
917    #[test]
918    fn test_validate_document_id_invalid_control_characters() {
919        assert!(validate_document_id("file\nname").is_err());
920        assert!(validate_document_id("file\x00name").is_err());
921    }
922
923    #[test]
924    fn test_validate_document_id_invalid_windows_reserved_characters() {
925        assert!(validate_document_id("file<name>").is_err());
926        assert!(validate_document_id("file>name").is_err());
927        assert!(validate_document_id("file:name").is_err());
928        assert!(validate_document_id("file\"name").is_err());
929        assert!(validate_document_id("file|name").is_err());
930        assert!(validate_document_id("file?name").is_err());
931        assert!(validate_document_id("file*name").is_err());
932    }
933
934    #[test]
935    fn test_validate_document_id_invalid_other_characters() {
936        assert!(validate_document_id("file name").is_err()); // space
937        assert!(validate_document_id("file@name").is_err()); // @
938        assert!(validate_document_id("file!name").is_err()); // !
939        assert!(validate_document_id("filešŸš€name").is_err()); // emoji
940        assert!(validate_document_id("fileƩname").is_err()); // accented
941        assert!(validate_document_id("file.name").is_err()); // dot
942    }
943
944    #[test]
945    fn test_validate_document_id_invalid_windows_reserved_names() {
946        // Test reserved names (case-insensitive)
947        assert!(validate_document_id("CON").is_err());
948        assert!(validate_document_id("con").is_err());
949        assert!(validate_document_id("Con").is_err());
950        assert!(validate_document_id("PRN").is_err());
951        assert!(validate_document_id("AUX").is_err());
952        assert!(validate_document_id("NUL").is_err());
953        assert!(validate_document_id("COM1").is_err());
954        assert!(validate_document_id("LPT9").is_err());
955
956        // Test with extensions
957        assert!(validate_document_id("CON.txt").is_err());
958        assert!(validate_document_id("prn.backup").is_err());
959    }
960
961    #[tokio::test]
962    async fn test_insert_invalid_document_id() {
963        let (collection, _temp_dir) = setup_collection().await;
964
965        let doc = json!({ "data": "test" });
966
967        // Test empty ID
968        assert!(collection.insert("", doc.clone()).await.is_err());
969
970        // Test Windows reserved name
971        assert!(collection.insert("CON", doc.clone()).await.is_err());
972
973        // Test invalid character
974        assert!(collection.insert("file name", doc.clone()).await.is_err());
975    }
976
977    #[tokio::test]
978    async fn test_get_corrupted_json() {
979        let (collection, _temp_dir) = setup_collection().await;
980
981        // Manually create a file with invalid JSON
982        let file_path = collection.path.join("corrupted.json");
983        tokio_fs::write(&file_path, "{ invalid json }")
984            .await
985            .unwrap();
986
987        let result = collection.get("corrupted").await;
988        assert!(result.is_err());
989    }
990
991    #[tokio::test]
992    async fn test_update_invalid_document_id() {
993        let (collection, _temp_dir) = setup_collection().await;
994
995        let doc = json!({ "data": "test" });
996
997        // Test empty ID
998        assert!(collection.update("", doc.clone()).await.is_err());
999
1000        // Test Windows reserved name
1001        assert!(collection.update("CON", doc.clone()).await.is_err());
1002    }
1003
1004    #[tokio::test]
1005    async fn test_delete_invalid_document_id() {
1006        let (collection, _temp_dir) = setup_collection().await;
1007
1008        // Test empty ID
1009        assert!(collection.delete("").await.is_err());
1010
1011        // Test Windows reserved name
1012        assert!(collection.delete("CON").await.is_err());
1013    }
1014}