1use std::{path::PathBuf, sync::Arc};
2
3use async_stream::stream;
4use futures::{StreamExt as _, TryStreamExt as _};
5use serde_json::{json, Value};
6use tokio::fs as tokio_fs;
7use tokio_stream::Stream;
8use tracing::{debug, error, trace, warn};
9
10use crate::{
11 comparison::compare_values,
12 filtering::matches_filters,
13 projection::project_document,
14 query::{Aggregation, Filter},
15 streaming::stream_document_ids,
16 validation::{is_reserved_name, is_valid_document_id_chars},
17 Document,
18 Result,
19 SentinelError,
20};
21
22#[derive(Debug, Clone, PartialEq, Eq)]
80#[allow(
81 clippy::field_scoped_visibility_modifiers,
82 reason = "fields need to be pub(crate) for internal access"
83)]
84pub struct Collection {
85 pub(crate) path: PathBuf,
87 pub(crate) signing_key: Option<Arc<sentinel_crypto::SigningKey>>,
89}
90
91#[allow(
92 unexpected_cfgs,
93 reason = "tarpaulin_include is set by code coverage tool"
94)]
95impl Collection {
96 pub fn name(&self) -> &str { self.path.file_name().unwrap().to_str().unwrap() }
98
99 pub async fn insert(&self, id: &str, data: Value) -> Result<()> {
137 trace!("Inserting document with id: {}", id);
138 validate_document_id(id)?;
139 let file_path = self.path.join(format!("{}.json", id));
140
141 #[allow(clippy::pattern_type_mismatch, reason = "false positive")]
142 let doc = if let Some(key) = &self.signing_key {
143 debug!("Creating signed document for id: {}", id);
144 Document::new(id.to_owned(), data, key).await?
145 }
146 else {
147 debug!("Creating unsigned document for id: {}", id);
148 Document::new_without_signature(id.to_owned(), data).await?
149 };
150
151 #[cfg(not(tarpaulin_include))]
156 let json = serde_json::to_string_pretty(&doc).map_err(|e| {
157 error!("Failed to serialize document {} to JSON: {}", id, e);
158 e
159 })?;
160
161 tokio_fs::write(&file_path, json).await.map_err(|e| {
162 error!(
163 "Failed to write document {} to file {:?}: {}",
164 id, file_path, e
165 );
166 e
167 })?;
168 debug!("Document {} inserted successfully", id);
169 Ok(())
170 }
171
172 pub async fn get(&self, id: &str) -> Result<Option<Document>> {
216 self.get_with_verification(id, &crate::VerificationOptions::default())
217 .await
218 }
219
220 pub async fn get_with_verification(
264 &self,
265 id: &str,
266 options: &crate::VerificationOptions,
267 ) -> Result<Option<Document>> {
268 trace!(
269 "Retrieving document with id: {} (verification enabled: {})",
270 id,
271 options.verify_signature || options.verify_hash
272 );
273 validate_document_id(id)?;
274 let file_path = self.path.join(format!("{}.json", id));
275 match tokio_fs::read_to_string(&file_path).await {
276 Ok(content) => {
277 debug!("Document {} found, parsing JSON", id);
278 let mut doc: Document = serde_json::from_str(&content).map_err(|e| {
279 error!("Failed to parse JSON for document {}: {}", id, e);
280 e
281 })?;
282 doc.id = id.to_owned();
284
285 self.verify_document(&doc, options).await?;
286
287 trace!("Document {} retrieved successfully", id);
288 Ok(Some(doc))
289 },
290 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
291 debug!("Document {} not found", id);
292 Ok(None)
293 },
294 Err(e) => {
295 error!("IO error reading document {}: {}", id, e);
296 Err(SentinelError::Io {
297 source: e,
298 })
299 },
300 }
301 }
302
303 pub async fn delete(&self, id: &str) -> Result<()> {
347 trace!("Deleting document with id: {}", id);
348 validate_document_id(id)?;
349 let source_path = self.path.join(format!("{}.json", id));
350 let deleted_dir = self.path.join(".deleted");
351 let dest_path = deleted_dir.join(format!("{}.json", id));
352
353 match tokio_fs::metadata(&source_path).await {
355 Ok(_) => {
356 debug!("Document {} exists, moving to .deleted", id);
357 tokio_fs::create_dir_all(&deleted_dir).await.map_err(|e| {
359 error!(
360 "Failed to create .deleted directory {:?}: {}",
361 deleted_dir, e
362 );
363 e
364 })?;
365 tokio_fs::rename(&source_path, &dest_path)
367 .await
368 .map_err(|e| {
369 error!("Failed to move document {} to .deleted: {}", id, e);
370 e
371 })?;
372 debug!("Document {} soft deleted successfully", id);
373 Ok(())
374 },
375 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
376 debug!(
377 "Document {} not found, already deleted or never existed",
378 id
379 );
380 Ok(())
381 },
382 Err(e) => {
383 error!("IO error checking document {} existence: {}", id, e);
384 Err(SentinelError::Io {
385 source: e,
386 })
387 },
388 }
389 }
390
391 pub fn list(&self) -> std::pin::Pin<Box<dyn Stream<Item = Result<String>> + Send>> {
426 trace!("Streaming document IDs from collection: {}", self.name());
427 stream_document_ids(self.path.clone())
428 }
429
430 pub async fn count(&self) -> Result<usize> {
461 trace!("Counting documents in collection: {}", self.name());
462 let ids: Vec<String> = self.list().try_collect().await?;
463 Ok(ids.len())
464 }
465
466 pub async fn bulk_insert(&self, documents: Vec<(&str, Value)>) -> Result<()> {
509 let count = documents.len();
510 trace!(
511 "Bulk inserting {} documents into collection {}",
512 count,
513 self.name()
514 );
515 for (id, data) in documents {
516 self.insert(id, data).await?;
517 }
518 debug!("Bulk insert of {} documents completed successfully", count);
519 Ok(())
520 }
521
522 pub fn filter<F>(&self, predicate: F) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
573 where
574 F: Fn(&Document) -> bool + Send + Sync + 'static,
575 {
576 self.filter_with_verification(predicate, &crate::VerificationOptions::default())
577 }
578
579 pub fn filter_with_verification<F>(
633 &self,
634 predicate: F,
635 options: &crate::VerificationOptions,
636 ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
637 where
638 F: Fn(&Document) -> bool + Send + Sync + 'static,
639 {
640 let collection_path = self.path.clone();
641 let signing_key = self.signing_key.clone();
642 let options = *options;
643
644 Box::pin(stream! {
645 trace!(
646 "Streaming filter on collection (verification enabled: {})",
647 options.verify_signature || options.verify_hash
648 );
649 let mut entries = match tokio_fs::read_dir(&collection_path).await {
650 Ok(entries) => entries,
651 Err(e) => {
652 yield Err(e.into());
653 return;
654 }
655 };
656
657 loop {
658 let entry = match entries.next_entry().await {
659 Ok(Some(entry)) => entry,
660 Ok(None) => break,
661 Err(e) => {
662 yield Err(e.into());
663 continue;
664 }
665 };
666
667 let path = entry.path();
668 if !tokio_fs::metadata(&path).await.map(|m| m.is_dir()).unwrap_or(false)
669 && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
670 && file_name.ends_with(".json") && !file_name.starts_with('.') {
671 let id = file_name.strip_suffix(".json").unwrap();
672 let file_path = collection_path.join(format!("{}.json", id));
673 match tokio_fs::read_to_string(&file_path).await {
674 Ok(content) => {
675 match serde_json::from_str::<Document>(&content) {
676 Ok(mut doc) => {
677 doc.id = id.to_owned();
678
679 let collection_ref = Self {
680 path: collection_path.clone(),
681 signing_key: signing_key.clone(),
682 };
683
684 if let Err(e) = collection_ref.verify_document(&doc, &options).await {
685 if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
686 if options.hash_verification_mode == crate::VerificationMode::Strict
687 || options.signature_verification_mode == crate::VerificationMode::Strict
688 {
689 yield Err(e);
690 continue;
691 }
692 } else {
693 yield Err(e);
694 continue;
695 }
696 }
697
698 if predicate(&doc) {
699 yield Ok(doc);
700 }
701 }
702 Err(e) => yield Err(e.into()),
703 }
704 }
705 Err(e) => yield Err(e.into()),
706 }
707 }
708 }
709 debug!("Streaming filter completed");
710 })
711 }
712
713 pub fn all(&self) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>> {
745 self.all_with_verification(&crate::VerificationOptions::default())
746 }
747
748 pub fn all_with_verification(
782 &self,
783 options: &crate::VerificationOptions,
784 ) -> std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>> {
785 let collection_path = self.path.clone();
786 let signing_key = self.signing_key.clone();
787 let options = *options;
788
789 Box::pin(stream! {
790 trace!(
791 "Streaming all documents on collection (verification enabled: {})",
792 options.verify_signature || options.verify_hash
793 );
794 let mut entries = match tokio_fs::read_dir(&collection_path).await {
795 Ok(entries) => entries,
796 Err(e) => {
797 yield Err(e.into());
798 return;
799 }
800 };
801
802 loop {
803 let entry = match entries.next_entry().await {
804 Ok(Some(entry)) => entry,
805 Ok(None) => break,
806 Err(e) => {
807 yield Err(e.into());
808 continue;
809 }
810 };
811
812 let path = entry.path();
813 if !tokio_fs::metadata(&path).await.map(|m| m.is_dir()).unwrap_or(false)
814 && let Some(file_name) = path.file_name().and_then(|n| n.to_str())
815 && file_name.ends_with(".json") && !file_name.starts_with('.') {
816 let id = file_name.strip_suffix(".json").unwrap();
817 let file_path = collection_path.join(format!("{}.json", id));
818 match tokio_fs::read_to_string(&file_path).await {
819 Ok(content) => {
820 match serde_json::from_str::<Document>(&content) {
821 Ok(mut doc) => {
822 doc.id = id.to_owned();
823
824 let collection_ref = Self {
825 path: collection_path.clone(),
826 signing_key: signing_key.clone(),
827 };
828
829 if let Err(e) = collection_ref.verify_document(&doc, &options).await {
830 if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
831 if options.hash_verification_mode == crate::VerificationMode::Strict
832 || options.signature_verification_mode == crate::VerificationMode::Strict
833 {
834 yield Err(e);
835 continue;
836 }
837 } else {
838 yield Err(e);
839 continue;
840 }
841 }
842
843 yield Ok(doc);
844 }
845 Err(e) => yield Err(e.into()),
846 }
847 }
848 Err(e) => yield Err(e.into()),
849 }
850 }
851 }
852 debug!("Streaming all completed");
853 })
854 }
855
856 pub async fn query(&self, query: crate::Query) -> Result<crate::QueryResult> {
905 self.query_with_verification(query, &crate::VerificationOptions::default())
906 .await
907 }
908
909 pub async fn query_with_verification(
957 &self,
958 query: crate::Query,
959 options: &crate::VerificationOptions,
960 ) -> Result<crate::QueryResult> {
961 use std::time::Instant;
962 let start_time = Instant::now();
963
964 trace!(
965 "Executing query on collection: {} (verification enabled: {})",
966 self.name(),
967 options.verify_signature || options.verify_hash
968 );
969
970 let documents_stream = if query.sort.is_some() {
974 let all_ids: Vec<String> = self.list().try_collect().await?;
976 let docs = self
977 .execute_sorted_query_with_verification(&all_ids, &query, options)
978 .await?;
979 let stream = tokio_stream::iter(docs.into_iter().map(Ok));
980 Box::pin(stream) as std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>
981 }
982 else {
983 self.execute_streaming_query_with_verification(&query, options)
985 .await?
986 };
987
988 let execution_time = start_time.elapsed();
989 debug!("Query completed in {:?}", execution_time);
990
991 Ok(crate::QueryResult {
992 documents: documents_stream,
993 total_count: None, execution_time,
995 })
996 }
997
998 async fn execute_sorted_query_with_verification(
1001 &self,
1002 all_ids: &[String],
1003 query: &crate::Query,
1004 options: &crate::VerificationOptions,
1005 ) -> Result<Vec<Document>> {
1006 let mut matching_docs = Vec::new();
1009
1010 let filter_refs: Vec<_> = query.filters.iter().collect();
1012
1013 for id in all_ids {
1014 if let Some(doc) = self.get_with_verification(id, options).await? &&
1015 matches_filters(&doc, &filter_refs)
1016 {
1017 matching_docs.push(doc);
1018 }
1019 }
1020
1021 if let Some(ref inner) = query.sort {
1022 let field = &inner.0;
1023 let order = &inner.1;
1024 matching_docs.sort_by(|a, b| {
1025 let a_val = a.data().get(field.as_str());
1026 let b_val = b.data().get(field.as_str());
1027 if *order == crate::SortOrder::Ascending {
1028 self.compare_values(a_val, b_val)
1029 }
1030 else {
1031 self.compare_values(b_val, a_val)
1032 }
1033 });
1034 }
1035
1036 let offset = query.offset.unwrap_or(0);
1038 let start_idx = offset.min(matching_docs.len());
1039 let end_idx = query.limit.map_or(matching_docs.len(), |limit| {
1040 start_idx.saturating_add(limit).min(matching_docs.len())
1041 });
1042
1043 let mut final_docs = Vec::new();
1045 for doc in matching_docs
1046 .into_iter()
1047 .skip(start_idx)
1048 .take(end_idx.saturating_sub(start_idx))
1049 {
1050 let projected_doc = if let Some(ref fields) = query.projection {
1051 self.project_document(&doc, fields).await?
1052 }
1053 else {
1054 doc
1055 };
1056 final_docs.push(projected_doc);
1057 }
1058
1059 Ok(final_docs)
1060 }
1061
1062 async fn execute_streaming_query_with_verification(
1065 &self,
1066 query: &crate::Query,
1067 options: &crate::VerificationOptions,
1068 ) -> Result<std::pin::Pin<Box<dyn Stream<Item = Result<Document>> + Send>>> {
1069 let collection_path = self.path.clone();
1070 let signing_key = self.signing_key.clone();
1071 let filters = query.filters.clone();
1072 let projection_fields = query.projection.clone();
1073 let limit = query.limit.unwrap_or(usize::MAX);
1074 let offset = query.offset.unwrap_or(0);
1075 let options = *options;
1076
1077 Ok(Box::pin(stream! {
1078 let mut id_stream = stream_document_ids(collection_path.clone());
1079 let mut yielded = 0;
1080 let mut skipped = 0;
1081
1082 let filter_refs: Vec<_> = filters.iter().collect();
1084
1085 while let Some(id_result) = id_stream.next().await {
1086 let id = match id_result {
1087 Ok(id) => id,
1088 Err(e) => {
1089 yield Err(e);
1090 continue;
1091 }
1092 };
1093
1094 let file_path = collection_path.join(format!("{}.json", id));
1096 let content = match tokio_fs::read_to_string(&file_path).await {
1097 Ok(content) => content,
1098 Err(e) => {
1099 yield Err(e.into());
1100 continue;
1101 }
1102 };
1103
1104 let doc = match serde_json::from_str::<Document>(&content) {
1105 Ok(doc) => {
1106 let mut doc_with_id = doc;
1108 doc_with_id.id = id.clone();
1109
1110 let collection_ref = Self {
1111 path: collection_path.clone(),
1112 signing_key: signing_key.clone(),
1113 };
1114
1115 if let Err(e) = collection_ref.verify_document(&doc_with_id, &options).await {
1116 if matches!(e, SentinelError::HashVerificationFailed { .. } | SentinelError::SignatureVerificationFailed { .. }) {
1117 if options.hash_verification_mode == crate::VerificationMode::Strict
1118 || options.signature_verification_mode == crate::VerificationMode::Strict
1119 {
1120 yield Err(e);
1121 continue;
1122 }
1123 } else {
1124 yield Err(e);
1125 continue;
1126 }
1127 }
1128
1129 doc_with_id
1130 }
1131 Err(e) => {
1132 yield Err(e.into());
1133 continue;
1134 }
1135 };
1136
1137 if matches_filters(&doc, &filter_refs) {
1138 if skipped < offset {
1139 skipped = skipped.saturating_add(1);
1140 continue;
1141 }
1142 if yielded >= limit {
1143 break;
1144 }
1145 let final_doc = if let Some(ref fields) = projection_fields {
1146 project_document(&doc, fields).await?
1147 } else {
1148 doc
1149 };
1150 yield Ok(final_doc);
1151 yielded = yielded.saturating_add(1);
1152 }
1153 }
1154 }))
1155 }
1156
1157 fn compare_values(&self, a: Option<&Value>, b: Option<&Value>) -> std::cmp::Ordering { compare_values(a, b) }
1159
1160 async fn project_document(&self, doc: &Document, fields: &[String]) -> Result<Document> {
1162 project_document(doc, fields).await
1163 }
1164
1165 async fn verify_hash(&self, doc: &Document, options: crate::VerificationOptions) -> Result<()> {
1177 if options.hash_verification_mode == crate::VerificationMode::Silent {
1178 return Ok(());
1179 }
1180
1181 trace!("Verifying hash for document: {}", doc.id());
1182 let computed_hash = sentinel_crypto::hash_data(doc.data()).await?;
1183
1184 if computed_hash != doc.hash() {
1185 let reason = format!(
1186 "Expected hash: {}, Computed hash: {}",
1187 doc.hash(),
1188 computed_hash
1189 );
1190
1191 match options.hash_verification_mode {
1192 crate::VerificationMode::Strict => {
1193 error!("Document {} hash verification failed: {}", doc.id(), reason);
1194 return Err(SentinelError::HashVerificationFailed {
1195 id: doc.id().to_owned(),
1196 reason,
1197 });
1198 },
1199 crate::VerificationMode::Warn => {
1200 warn!("Document {} hash verification failed: {}", doc.id(), reason);
1201 },
1202 crate::VerificationMode::Silent => {},
1203 }
1204 }
1205 else {
1206 trace!("Document {} hash verified successfully", doc.id());
1207 }
1208
1209 Ok(())
1210 }
1211
1212 async fn verify_signature(&self, doc: &Document, options: crate::VerificationOptions) -> Result<()> {
1224 if options.signature_verification_mode == crate::VerificationMode::Silent &&
1225 options.empty_signature_mode == crate::VerificationMode::Silent
1226 {
1227 return Ok(());
1228 }
1229
1230 trace!("Verifying signature for document: {}", doc.id());
1231
1232 if doc.signature().is_empty() {
1233 let reason = "Document has no signature".to_owned();
1234
1235 match options.empty_signature_mode {
1236 crate::VerificationMode::Strict => {
1237 error!("Document {} has no signature: {}", doc.id(), reason);
1238 return Err(SentinelError::SignatureVerificationFailed {
1239 id: doc.id().to_owned(),
1240 reason,
1241 });
1242 },
1243 crate::VerificationMode::Warn => {
1244 warn!("Document {} has no signature: {}", doc.id(), reason);
1245 },
1246 crate::VerificationMode::Silent => {},
1247 }
1248 return Ok(());
1249 }
1250
1251 if !options.verify_signature {
1252 trace!("Signature verification disabled for document: {}", doc.id());
1253 return Ok(());
1254 }
1255
1256 if let Some(ref signing_key) = self.signing_key {
1257 let public_key = signing_key.verifying_key();
1258 let is_valid = sentinel_crypto::verify_signature(doc.hash(), doc.signature(), &public_key).await?;
1259
1260 if !is_valid {
1261 let reason = "Signature verification using public key failed".to_owned();
1262
1263 match options.signature_verification_mode {
1264 crate::VerificationMode::Strict => {
1265 error!(
1266 "Document {} signature verification failed: {}",
1267 doc.id(),
1268 reason
1269 );
1270 return Err(SentinelError::SignatureVerificationFailed {
1271 id: doc.id().to_owned(),
1272 reason,
1273 });
1274 },
1275 crate::VerificationMode::Warn => {
1276 warn!(
1277 "Document {} signature verification failed: {}",
1278 doc.id(),
1279 reason
1280 );
1281 },
1282 crate::VerificationMode::Silent => {},
1283 }
1284 }
1285 else {
1286 trace!("Document {} signature verified successfully", doc.id());
1287 }
1288 }
1289 else {
1290 trace!("No signing key available for verification, skipping signature check");
1291 }
1292
1293 Ok(())
1294 }
1295
1296 async fn verify_document(&self, doc: &Document, options: &crate::VerificationOptions) -> Result<()> {
1308 if options.verify_hash {
1309 self.verify_hash(doc, *options).await?;
1310 }
1311
1312 if options.verify_signature {
1313 self.verify_signature(doc, *options).await?;
1314 }
1315
1316 Ok(())
1317 }
1318
1319 #[allow(
1366 clippy::pattern_type_mismatch,
1367 reason = "false positive with serde_json::Value"
1368 )]
1369 fn merge_json_values(existing_value: &Value, new_value: Value) -> Value {
1370 match (existing_value, &new_value) {
1371 (Value::Object(existing_map), Value::Object(new_map)) => {
1372 let mut merged = existing_map.clone();
1373 for (key, value) in new_map {
1374 merged.insert(key.clone(), value.clone());
1375 }
1376 Value::Object(merged)
1377 },
1378 _ => new_value,
1379 }
1380 }
1381
1382 fn extract_numeric_value(doc: &Document, field: &str) -> Option<f64> {
1384 doc.data().get(field).and_then(|v| {
1385 match *v {
1386 Value::Number(ref n) => n.as_f64(),
1387 Value::Null | Value::Bool(_) | Value::String(_) | Value::Array(_) | Value::Object(_) => None,
1388 }
1389 })
1390 }
1391
1392 pub async fn update(&self, id: &str, data: Value) -> Result<()> {
1393 trace!("Updating document with id: {}", id);
1394 validate_document_id(id)?;
1395
1396 let Some(mut existing_doc) = self.get(id).await?
1398 else {
1399 return Err(SentinelError::DocumentNotFound {
1400 id: id.to_owned(),
1401 collection: self.name().to_owned(),
1402 });
1403 };
1404
1405 let merged_data = Self::merge_json_values(existing_doc.data(), data);
1407
1408 if let Some(key) = self.signing_key.as_ref() {
1410 existing_doc.set_data(merged_data, key).await?;
1411 }
1412 else {
1413 existing_doc.data = merged_data;
1415 existing_doc.updated_at = chrono::Utc::now();
1416 existing_doc.hash = sentinel_crypto::hash_data(&existing_doc.data).await?;
1417 existing_doc.signature = String::new();
1418 }
1419
1420 let file_path = self.path.join(format!("{}.json", id));
1422 let json = serde_json::to_string_pretty(&existing_doc).map_err(|e| {
1423 error!("Failed to serialize updated document {} to JSON: {}", id, e);
1424 e
1425 })?;
1426 tokio_fs::write(&file_path, json).await.map_err(|e| {
1427 error!(
1428 "Failed to write updated document {} to file {:?}: {}",
1429 id, file_path, e
1430 );
1431 e
1432 })?;
1433
1434 debug!("Document {} updated successfully", id);
1435 Ok(())
1436 }
1437
1438 pub async fn get_many(&self, ids: &[&str]) -> Result<Vec<Option<Document>>> {
1477 use futures::future::join_all;
1478
1479 trace!("Batch getting {} documents", ids.len());
1480
1481 let futures = ids.iter().map(|&id| self.get(id));
1482 let results = join_all(futures).await;
1483
1484 let documents = results.into_iter().collect::<Result<Vec<_>>>()?;
1485
1486 debug!(
1487 "Batch get completed, retrieved {} documents",
1488 documents.len()
1489 );
1490 Ok(documents)
1491 }
1492
1493 pub async fn upsert(&self, id: &str, data: Value) -> Result<bool> {
1535 trace!("Upserting document with id: {}", id);
1536
1537 if self.get(id).await?.is_some() {
1538 self.update(id, data).await?;
1540 debug!("Document {} updated via upsert", id);
1541 Ok(false)
1542 }
1543 else {
1544 self.insert(id, data).await?;
1546 debug!("Document {} inserted via upsert", id);
1547 Ok(true)
1548 }
1549 }
1550
1551 pub async fn aggregate(&self, filters: Vec<crate::Filter>, aggregation: Aggregation) -> Result<Value> {
1594 use futures::TryStreamExt as _;
1595
1596 trace!("Performing aggregation: {:?}", aggregation);
1597
1598 let mut stream = self.all();
1600
1601 let mut count = 0usize;
1602 let mut sum = 0.0f64;
1603 let mut min = f64::INFINITY;
1604 let mut max = f64::NEG_INFINITY;
1605
1606 while let Some(doc) = stream.try_next().await? {
1607 if !filters.is_empty() {
1609 let filter_refs: Vec<&Filter> = filters.iter().collect();
1610 if !crate::filtering::matches_filters(&doc, &filter_refs) {
1611 continue;
1612 }
1613 }
1614
1615 count = count.saturating_add(1);
1616
1617 if let Aggregation::Sum(ref field) |
1619 Aggregation::Avg(ref field) |
1620 Aggregation::Min(ref field) |
1621 Aggregation::Max(ref field) = aggregation &&
1622 let Some(value) = Self::extract_numeric_value(&doc, field)
1623 {
1624 sum += value;
1625 min = min.min(value);
1626 max = max.max(value);
1627 }
1628 }
1629
1630 let result = match aggregation {
1631 Aggregation::Count => json!(count),
1632 Aggregation::Sum(_) => json!(sum),
1633 Aggregation::Avg(_) => {
1634 if count == 0 {
1635 json!(null)
1636 }
1637 else {
1638 json!(sum / count as f64)
1639 }
1640 },
1641 Aggregation::Min(_) => {
1642 if min == f64::INFINITY {
1643 json!(null)
1644 }
1645 else {
1646 json!(min)
1647 }
1648 },
1649 Aggregation::Max(_) => {
1650 if max == f64::NEG_INFINITY {
1651 json!(null)
1652 }
1653 else {
1654 json!(max)
1655 }
1656 },
1657 };
1658
1659 debug!("Aggregation result: {}", result);
1660 Ok(result)
1661 }
1662}
1663
1664pub fn validate_document_id(id: &str) -> Result<()> {
1681 trace!("Validating document id: {}", id);
1682 if id.is_empty() {
1684 warn!("Document id is empty");
1685 return Err(SentinelError::InvalidDocumentId {
1686 id: id.to_owned(),
1687 });
1688 }
1689
1690 if !is_valid_document_id_chars(id) {
1692 warn!("Document id contains invalid characters: {}", id);
1693 return Err(SentinelError::InvalidDocumentId {
1694 id: id.to_owned(),
1695 });
1696 }
1697
1698 if is_reserved_name(id) {
1700 warn!("Document id is a reserved name: {}", id);
1701 return Err(SentinelError::InvalidDocumentId {
1702 id: id.to_owned(),
1703 });
1704 }
1705
1706 trace!("Document id '{}' is valid", id);
1707 Ok(())
1708}
1709
1710#[cfg(test)]
1711mod tests {
1712 use serde_json::json;
1713 use tempfile::tempdir;
1714 use tracing_subscriber;
1715
1716 use super::*;
1717 use crate::Store;
1718
1719 async fn setup_collection() -> (Collection, tempfile::TempDir) {
1721 let _ = tracing_subscriber::fmt()
1723 .with_env_filter("debug")
1724 .try_init();
1725
1726 let temp_dir = tempdir().unwrap();
1727 let store = Store::new(temp_dir.path(), None).await.unwrap();
1728 let collection = store.collection("test_collection").await.unwrap();
1729 (collection, temp_dir)
1730 }
1731
1732 async fn setup_collection_with_signing_key() -> (Collection, tempfile::TempDir) {
1734 let _ = tracing_subscriber::fmt()
1736 .with_env_filter("debug")
1737 .try_init();
1738
1739 let temp_dir = tempdir().unwrap();
1740 let store = Store::new(temp_dir.path(), Some("test_passphrase"))
1741 .await
1742 .unwrap();
1743 let collection = store.collection("test_collection").await.unwrap();
1744 (collection, temp_dir)
1745 }
1746
1747 #[tokio::test]
1748 async fn test_insert_and_retrieve() {
1749 let (collection, _temp_dir) = setup_collection().await;
1750
1751 let doc = json!({ "name": "Alice", "email": "alice@example.com" });
1752 collection.insert("user-123", doc.clone()).await.unwrap();
1753
1754 let retrieved = collection
1755 .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1756 .await
1757 .unwrap();
1758 assert_eq!(*retrieved.unwrap().data(), doc);
1759 }
1760
1761 #[tokio::test]
1762 async fn test_insert_empty_document() {
1763 let (collection, _temp_dir) = setup_collection().await;
1764
1765 let doc = json!({});
1766 collection.insert("empty", doc.clone()).await.unwrap();
1767
1768 let retrieved = collection
1769 .get_with_verification("empty", &crate::VerificationOptions::disabled())
1770 .await
1771 .unwrap();
1772 assert_eq!(*retrieved.unwrap().data(), doc);
1773 }
1774
1775 #[tokio::test]
1776 async fn test_insert_with_signing_key() {
1777 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
1778
1779 let doc = json!({ "name": "Alice", "signed": true });
1780 collection.insert("signed_doc", doc.clone()).await.unwrap();
1781
1782 let retrieved = collection.get("signed_doc").await.unwrap().unwrap();
1783 assert_eq!(*retrieved.data(), doc);
1784 assert!(!retrieved.signature().is_empty());
1786 }
1787
1788 #[tokio::test]
1789 async fn test_insert_large_document() {
1790 let (collection, _temp_dir) = setup_collection().await;
1791
1792 let large_data = json!({
1793 "large_array": (0..1000).collect::<Vec<_>>(),
1794 "nested": {
1795 "deep": {
1796 "value": "test"
1797 }
1798 }
1799 });
1800 collection
1801 .insert("large", large_data.clone())
1802 .await
1803 .unwrap();
1804
1805 let retrieved = collection
1806 .get_with_verification("large", &crate::VerificationOptions::disabled())
1807 .await
1808 .unwrap();
1809 assert_eq!(*retrieved.unwrap().data(), large_data);
1810 }
1811
1812 #[tokio::test]
1813 async fn test_insert_with_invalid_special_characters_in_id() {
1814 let (collection, _temp_dir) = setup_collection().await;
1815
1816 let doc = json!({ "data": "test" });
1817 let result = collection.insert("user_123-special!", doc.clone()).await;
1818
1819 assert!(result.is_err());
1821 match result {
1822 Err(SentinelError::InvalidDocumentId {
1823 id,
1824 }) => {
1825 assert_eq!(id, "user_123-special!");
1826 },
1827 _ => panic!("Expected InvalidDocumentId error"),
1828 }
1829 }
1830
1831 #[tokio::test]
1832 async fn test_insert_with_valid_document_ids() {
1833 let (collection, _temp_dir) = setup_collection().await;
1834
1835 let valid_ids = vec![
1837 "user-123",
1838 "user_456",
1839 "user123",
1840 "123",
1841 "a",
1842 "user-123_test",
1843 "user_123-test",
1844 "CamelCaseID",
1845 "lower_case_id",
1846 "UPPER_CASE_ID",
1847 ];
1848
1849 for id in valid_ids {
1850 let doc = json!({ "data": "test" });
1851 let result = collection.insert(id, doc).await;
1852 assert!(
1853 result.is_ok(),
1854 "Expected ID '{}' to be valid but got error: {:?}",
1855 id,
1856 result
1857 );
1858 }
1859 }
1860
1861 #[tokio::test]
1862 async fn test_insert_with_various_invalid_document_ids() {
1863 let (collection, _temp_dir) = setup_collection().await;
1864
1865 let invalid_ids = vec![
1867 "user!123", "user@domain", "user#123", "user$123", "user%123", "user^123", "user&123", "user*123", "user(123)", "user.123", "user/123", "user\\123", "user:123", "user;123", "user<123", "user>123", "user?123", "user|123", "user\"123", "user'123", "", ];
1889
1890 for id in invalid_ids {
1891 let doc = json!({ "data": "test" });
1892 let result = collection.insert(id, doc).await;
1893 assert!(
1894 result.is_err(),
1895 "Expected ID '{}' to be invalid but insertion succeeded",
1896 id
1897 );
1898 match result {
1899 Err(SentinelError::InvalidDocumentId {
1900 ..
1901 }) => {
1902 },
1904 _ => panic!("Expected InvalidDocumentId error for ID '{}'", id),
1905 }
1906 }
1907 }
1908
1909 #[tokio::test]
1910 async fn test_get_nonexistent() {
1911 let (collection, _temp_dir) = setup_collection().await;
1912
1913 let retrieved = collection.get("nonexistent").await.unwrap();
1914 assert!(retrieved.is_none());
1915 }
1916
1917 #[tokio::test]
1918 async fn test_update() {
1919 let (collection, _temp_dir) = setup_collection().await;
1920
1921 let doc1 = json!({ "name": "Alice" });
1922 collection.insert("user-123", doc1).await.unwrap();
1923
1924 let doc2 = json!({ "name": "Alice", "age": 30 });
1925 collection.update("user-123", doc2.clone()).await.unwrap();
1926
1927 let retrieved = collection
1928 .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1929 .await
1930 .unwrap();
1931 assert_eq!(*retrieved.unwrap().data(), doc2);
1932 }
1933
1934 #[tokio::test]
1935 async fn test_update_nonexistent() {
1936 let (collection, _temp_dir) = setup_collection().await;
1937
1938 let doc = json!({ "name": "Bob" });
1939 let result = collection.update("new-user", doc.clone()).await;
1940
1941 assert!(result.is_err());
1943 match result.unwrap_err() {
1944 crate::SentinelError::DocumentNotFound {
1945 id,
1946 collection: _,
1947 } => {
1948 assert_eq!(id, "new-user");
1949 },
1950 _ => panic!("Expected DocumentNotFound error"),
1951 }
1952 }
1953
1954 #[tokio::test]
1955 async fn test_update_with_invalid_id() {
1956 let (collection, _temp_dir) = setup_collection().await;
1957
1958 let doc = json!({ "name": "Bob" });
1959 let result = collection.update("user!invalid", doc).await;
1960
1961 assert!(result.is_err());
1963 match result {
1964 Err(SentinelError::InvalidDocumentId {
1965 id,
1966 }) => {
1967 assert_eq!(id, "user!invalid");
1968 },
1969 _ => panic!("Expected InvalidDocumentId error"),
1970 }
1971 }
1972
1973 #[tokio::test]
1974 async fn test_delete() {
1975 let (collection, _temp_dir) = setup_collection().await;
1976
1977 let doc = json!({ "name": "Alice" });
1978 collection.insert("user-123", doc).await.unwrap();
1979
1980 let retrieved = collection
1981 .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1982 .await
1983 .unwrap();
1984 assert!(retrieved.is_some());
1985
1986 collection.delete("user-123").await.unwrap();
1987
1988 let retrieved = collection
1989 .get_with_verification("user-123", &crate::VerificationOptions::disabled())
1990 .await
1991 .unwrap();
1992 assert!(retrieved.is_none());
1993
1994 let deleted_path = collection.path.join(".deleted").join("user-123.json");
1996 assert!(tokio_fs::try_exists(&deleted_path).await.unwrap());
1997 }
1998
1999 #[tokio::test]
2000 async fn test_delete_nonexistent() {
2001 let (collection, _temp_dir) = setup_collection().await;
2002
2003 collection.delete("nonexistent").await.unwrap();
2005 }
2006
2007 #[tokio::test]
2008 async fn test_list_empty_collection() {
2009 let (collection, _temp_dir) = setup_collection().await;
2010
2011 let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2012 assert!(ids.is_empty());
2013 }
2014
2015 #[tokio::test]
2016 async fn test_list_with_documents() {
2017 let (collection, _temp_dir) = setup_collection().await;
2018
2019 collection
2020 .insert("user-123", json!({"name": "Alice"}))
2021 .await
2022 .unwrap();
2023 collection
2024 .insert("user-456", json!({"name": "Bob"}))
2025 .await
2026 .unwrap();
2027 collection
2028 .insert("user-789", json!({"name": "Charlie"}))
2029 .await
2030 .unwrap();
2031
2032 let mut ids: Vec<String> = collection.list().try_collect().await.unwrap();
2033 ids.sort(); assert_eq!(ids.len(), 3);
2035 assert_eq!(ids, vec!["user-123", "user-456", "user-789"]);
2036 }
2037
2038 #[tokio::test]
2039 async fn test_list_skips_deleted_documents() {
2040 let (collection, _temp_dir) = setup_collection().await;
2041
2042 collection
2043 .insert("user-123", json!({"name": "Alice"}))
2044 .await
2045 .unwrap();
2046 collection
2047 .insert("user-456", json!({"name": "Bob"}))
2048 .await
2049 .unwrap();
2050 collection.delete("user-456").await.unwrap();
2051
2052 let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2053 assert_eq!(ids.len(), 1);
2054 assert_eq!(ids, vec!["user-123"]);
2055 }
2056
2057 #[tokio::test]
2058 async fn test_bulk_insert() {
2059 let (collection, _temp_dir) = setup_collection().await;
2060
2061 let documents = vec![
2062 ("user-123", json!({"name": "Alice", "role": "admin"})),
2063 ("user-456", json!({"name": "Bob", "role": "user"})),
2064 ("user-789", json!({"name": "Charlie", "role": "user"})),
2065 ];
2066
2067 collection.bulk_insert(documents).await.unwrap();
2068
2069 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2070 assert_eq!(docs.len(), 3);
2071
2072 let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2073 assert_eq!(ids.len(), 3);
2074 assert!(ids.contains(&"user-123".to_string()));
2075 assert!(ids.contains(&"user-456".to_string()));
2076 assert!(ids.contains(&"user-789".to_string()));
2077
2078 let alice = collection
2080 .get_with_verification("user-123", &crate::VerificationOptions::disabled())
2081 .await
2082 .unwrap()
2083 .unwrap();
2084 assert_eq!(alice.data()["name"], "Alice");
2085 assert_eq!(alice.data()["role"], "admin");
2086 }
2087
2088 #[tokio::test]
2089 async fn test_bulk_insert_empty() {
2090 let (collection, _temp_dir) = setup_collection().await;
2091
2092 collection.bulk_insert(vec![]).await.unwrap();
2093
2094 let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2095 assert!(ids.is_empty());
2096 }
2097
2098 #[tokio::test]
2099 async fn test_bulk_insert_with_invalid_id() {
2100 let (collection, _temp_dir) = setup_collection().await;
2101
2102 let documents = vec![
2103 ("user-123", json!({"name": "Alice"})),
2104 ("user!invalid", json!({"name": "Bob"})),
2105 ];
2106
2107 let result = collection.bulk_insert(documents).await;
2108 assert!(result.is_err());
2109
2110 let ids: Vec<String> = collection.list().try_collect().await.unwrap();
2112 assert_eq!(ids.len(), 1);
2113 assert_eq!(ids[0], "user-123");
2114 }
2115
2116 #[tokio::test]
2117 async fn test_multiple_operations() {
2118 let (collection, _temp_dir) = setup_collection().await;
2119
2120 collection
2122 .insert("user1", json!({"name": "User1"}))
2123 .await
2124 .unwrap();
2125 collection
2126 .insert("user2", json!({"name": "User2"}))
2127 .await
2128 .unwrap();
2129
2130 let user1 = collection
2132 .get_with_verification("user1", &crate::VerificationOptions::disabled())
2133 .await
2134 .unwrap()
2135 .unwrap();
2136 let user2 = collection
2137 .get_with_verification("user2", &crate::VerificationOptions::disabled())
2138 .await
2139 .unwrap()
2140 .unwrap();
2141 assert_eq!(user1.data()["name"], "User1");
2142 assert_eq!(user2.data()["name"], "User2");
2143
2144 collection
2146 .update("user1", json!({"name": "Updated"}))
2147 .await
2148 .unwrap();
2149 let updated = collection
2150 .get_with_verification("user1", &crate::VerificationOptions::disabled())
2151 .await
2152 .unwrap()
2153 .unwrap();
2154 assert_eq!(updated.data()["name"], "Updated");
2155
2156 collection.delete("user2").await.unwrap();
2158 assert!(collection
2159 .get_with_verification("user2", &crate::VerificationOptions::disabled())
2160 .await
2161 .unwrap()
2162 .is_none());
2163 assert!(collection
2164 .get_with_verification("user1", &crate::VerificationOptions::disabled())
2165 .await
2166 .unwrap()
2167 .is_some());
2168 }
2169
2170 #[test]
2171 fn test_validate_document_id_valid() {
2172 assert!(validate_document_id("user-123").is_ok());
2174 assert!(validate_document_id("user_456").is_ok());
2175 assert!(validate_document_id("data-item").is_ok());
2176 assert!(validate_document_id("test_collection_123").is_ok());
2177 assert!(validate_document_id("file-txt").is_ok());
2178 assert!(validate_document_id("a").is_ok());
2179 assert!(validate_document_id("123").is_ok());
2180 }
2181
2182 #[test]
2183 fn test_validate_document_id_invalid_empty() {
2184 assert!(validate_document_id("").is_err());
2185 }
2186
2187 #[test]
2188 fn test_validate_document_id_invalid_path_separators() {
2189 assert!(validate_document_id("path/traversal").is_err());
2190 assert!(validate_document_id("path\\traversal").is_err());
2191 }
2192
2193 #[test]
2194 fn test_validate_document_id_invalid_control_characters() {
2195 assert!(validate_document_id("file\nname").is_err());
2196 assert!(validate_document_id("file\x00name").is_err());
2197 }
2198
2199 #[test]
2200 fn test_validate_document_id_invalid_windows_reserved_characters() {
2201 assert!(validate_document_id("file<name>").is_err());
2202 assert!(validate_document_id("file>name").is_err());
2203 assert!(validate_document_id("file:name").is_err());
2204 assert!(validate_document_id("file\"name").is_err());
2205 assert!(validate_document_id("file|name").is_err());
2206 assert!(validate_document_id("file?name").is_err());
2207 assert!(validate_document_id("file*name").is_err());
2208 }
2209
2210 #[test]
2211 fn test_validate_document_id_invalid_other_characters() {
2212 assert!(validate_document_id("file name").is_err()); assert!(validate_document_id("file@name").is_err()); assert!(validate_document_id("file!name").is_err()); assert!(validate_document_id("file🚀name").is_err()); assert!(validate_document_id("fileéname").is_err()); assert!(validate_document_id("file.name").is_err()); }
2219
2220 #[test]
2221 fn test_validate_document_id_invalid_windows_reserved_names() {
2222 assert!(validate_document_id("CON").is_err());
2224 assert!(validate_document_id("con").is_err());
2225 assert!(validate_document_id("Con").is_err());
2226 assert!(validate_document_id("PRN").is_err());
2227 assert!(validate_document_id("AUX").is_err());
2228 assert!(validate_document_id("NUL").is_err());
2229 assert!(validate_document_id("COM1").is_err());
2230 assert!(validate_document_id("LPT9").is_err());
2231
2232 assert!(validate_document_id("CON.txt").is_err());
2234 assert!(validate_document_id("prn.backup").is_err());
2235 }
2236
2237 #[tokio::test]
2238 async fn test_insert_invalid_document_id() {
2239 let (collection, _temp_dir) = setup_collection().await;
2240
2241 let doc = json!({ "data": "test" });
2242
2243 assert!(collection.insert("", doc.clone()).await.is_err());
2245
2246 assert!(collection.insert("CON", doc.clone()).await.is_err());
2248
2249 assert!(collection.insert("file name", doc.clone()).await.is_err());
2251 }
2252
2253 #[tokio::test]
2254 async fn test_get_corrupted_json() {
2255 let (collection, _temp_dir) = setup_collection().await;
2256
2257 let file_path = collection.path.join("corrupted.json");
2259 tokio_fs::write(&file_path, "{ invalid json }")
2260 .await
2261 .unwrap();
2262
2263 let result = collection.get("corrupted").await;
2264 assert!(result.is_err());
2265 }
2266
2267 #[tokio::test]
2268 async fn test_update_invalid_document_id() {
2269 let (collection, _temp_dir) = setup_collection().await;
2270
2271 let doc = json!({ "data": "test" });
2272
2273 assert!(collection.update("", doc.clone()).await.is_err());
2275
2276 assert!(collection.update("CON", doc.clone()).await.is_err());
2278 }
2279
2280 #[tokio::test]
2281 async fn test_delete_invalid_document_id() {
2282 let (collection, _temp_dir) = setup_collection().await;
2283
2284 assert!(collection.delete("").await.is_err());
2286
2287 assert!(collection.delete("CON").await.is_err());
2289 }
2290
2291 #[tokio::test]
2292 async fn test_get_nonexistent_with_verification() {
2293 let (collection, _temp_dir) = setup_collection().await;
2294
2295 let options = crate::VerificationOptions::strict();
2296 let result = collection
2297 .get_with_verification("nonexistent", &options)
2298 .await;
2299 assert!(result.is_ok());
2300 assert!(result.unwrap().is_none());
2301 }
2302
2303 #[tokio::test]
2304 async fn test_get_with_verification_disabled() {
2305 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2306
2307 let doc = json!({ "name": "Alice", "data": "test" });
2308 collection.insert("test_doc", doc.clone()).await.unwrap();
2309
2310 let file_path = collection.path.join("test_doc.json");
2312 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2313 content = content.replace("test", "tampered");
2314 tokio_fs::write(&file_path, &content).await.unwrap();
2315
2316 let options = crate::VerificationOptions::disabled();
2318 let result = collection.get_with_verification("test_doc", &options).await;
2319 assert!(result.is_ok());
2320 let doc = result.unwrap().unwrap();
2321 assert_eq!(doc.data()["name"], "Alice");
2322 }
2323
2324 #[tokio::test]
2325 async fn test_get_with_verification_empty_signature_warn() {
2326 let (collection, _temp_dir) = setup_collection().await;
2327
2328 let doc = json!({ "name": "Unsigned" });
2330 collection
2331 .insert("unsigned_doc", doc.clone())
2332 .await
2333 .unwrap();
2334
2335 let options = crate::VerificationOptions {
2337 verify_signature: true,
2338 verify_hash: true,
2339 signature_verification_mode: crate::VerificationMode::Strict,
2340 empty_signature_mode: crate::VerificationMode::Warn,
2341 hash_verification_mode: crate::VerificationMode::Strict,
2342 };
2343 let result = collection
2344 .get_with_verification("unsigned_doc", &options)
2345 .await;
2346 assert!(result.is_ok());
2347 assert!(result.unwrap().is_some());
2348 }
2349
2350 #[tokio::test]
2351 async fn test_get_with_verification_empty_signature_strict() {
2352 let (collection, _temp_dir) = setup_collection().await;
2353
2354 let doc = json!({ "name": "Unsigned" });
2356 collection
2357 .insert("unsigned_doc", doc.clone())
2358 .await
2359 .unwrap();
2360
2361 let options = crate::VerificationOptions {
2363 verify_signature: true,
2364 verify_hash: true,
2365 signature_verification_mode: crate::VerificationMode::Strict,
2366 empty_signature_mode: crate::VerificationMode::Strict,
2367 hash_verification_mode: crate::VerificationMode::Strict,
2368 };
2369 let result = collection
2370 .get_with_verification("unsigned_doc", &options)
2371 .await;
2372 assert!(result.is_err());
2373 match result.unwrap_err() {
2374 crate::SentinelError::SignatureVerificationFailed {
2375 id,
2376 reason,
2377 } => {
2378 assert_eq!(id, "unsigned_doc");
2379 assert!(reason.contains("no signature"));
2380 },
2381 _ => panic!("Expected SignatureVerificationFailed"),
2382 }
2383 }
2384
2385 #[tokio::test]
2386 async fn test_all_empty_collection() {
2387 let (collection, _temp_dir) = setup_collection().await;
2388
2389 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2390 assert!(docs.is_empty());
2391 }
2392
2393 #[tokio::test]
2394 async fn test_all_with_multiple_documents() {
2395 let (collection, _temp_dir) = setup_collection().await;
2396
2397 for i in 0 .. 5 {
2398 let doc = json!({ "id": i, "name": format!("User{}", i) });
2399 collection
2400 .insert(&format!("user-{}", i), doc)
2401 .await
2402 .unwrap();
2403 }
2404
2405 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2406 assert_eq!(docs.len(), 5);
2407
2408 let ids: std::collections::HashSet<_> = docs.iter().map(|d| d.id().to_string()).collect();
2409 for i in 0 .. 5 {
2410 assert!(ids.contains(&format!("user-{}", i)));
2411 }
2412 }
2413
2414 #[tokio::test]
2415 async fn test_all_with_verification() {
2416 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2417
2418 for i in 0 .. 3 {
2419 let doc = json!({ "id": i });
2420 collection
2421 .insert(&format!("signed-{}", i), doc)
2422 .await
2423 .unwrap();
2424 }
2425
2426 let options = crate::VerificationOptions::strict();
2427 let docs: Vec<_> = collection
2428 .all_with_verification(&options)
2429 .try_collect()
2430 .await
2431 .unwrap();
2432 assert_eq!(docs.len(), 3);
2433 }
2434
2435 #[tokio::test]
2436 async fn test_filter_empty_result() {
2437 let (collection, _temp_dir) = setup_collection().await;
2438
2439 for i in 0 .. 3 {
2440 let doc = json!({ "id": i, "status": "active" });
2441 collection
2442 .insert(&format!("user-{}", i), doc)
2443 .await
2444 .unwrap();
2445 }
2446
2447 let results: Vec<_> = collection
2448 .filter(|doc| doc.data().get("status") == Some(&json!("inactive")))
2449 .try_collect()
2450 .await
2451 .unwrap();
2452 assert!(results.is_empty());
2453 }
2454
2455 #[tokio::test]
2456 async fn test_filter_with_all_matching() {
2457 let (collection, _temp_dir) = setup_collection().await;
2458
2459 for i in 0 .. 5 {
2460 let doc = json!({ "id": i, "active": true });
2461 collection
2462 .insert(&format!("user-{}", i), doc)
2463 .await
2464 .unwrap();
2465 }
2466
2467 let results: Vec<_> = collection
2468 .filter(|doc| doc.data().get("active") == Some(&json!(true)))
2469 .try_collect()
2470 .await
2471 .unwrap();
2472 assert_eq!(results.len(), 5);
2473 }
2474
2475 #[tokio::test]
2476 async fn test_filter_with_verification() {
2477 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2478
2479 for i in 0 .. 3 {
2480 let doc = json!({ "id": i, "active": true });
2481 collection
2482 .insert(&format!("signed-{}", i), doc)
2483 .await
2484 .unwrap();
2485 }
2486
2487 let options = crate::VerificationOptions::strict();
2488 let results: Vec<_> = collection
2489 .filter_with_verification(
2490 |doc| doc.data().get("active") == Some(&json!(true)),
2491 &options,
2492 )
2493 .try_collect()
2494 .await
2495 .unwrap();
2496 assert_eq!(results.len(), 3);
2497 }
2498
2499 #[tokio::test]
2500 async fn test_bulk_insert_empty_all() {
2501 let (collection, _temp_dir) = setup_collection().await;
2502
2503 let result = collection.bulk_insert(vec![]).await;
2504 assert!(result.is_ok());
2505
2506 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2507 assert!(docs.is_empty());
2508 }
2509
2510 #[tokio::test]
2511 async fn test_bulk_insert_large_batch() {
2512 let (collection, _temp_dir) = setup_collection().await;
2513
2514 let documents: Vec<(String, serde_json::Value)> = (0 .. 100)
2515 .map(|i| {
2516 let key = format!("user-{}", i);
2517 let value = json!({ "id": i, "data": format!("value{}", i) });
2518 (key, value)
2519 })
2520 .collect();
2521
2522 let documents_refs: Vec<(&str, serde_json::Value)> = documents
2524 .iter()
2525 .map(|(k, v)| (k.as_str(), v.clone()))
2526 .collect();
2527
2528 let result = collection.bulk_insert(documents_refs).await;
2530 assert!(result.is_ok());
2531
2532 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2534 assert_eq!(docs.len(), 100);
2535 }
2536
2537 #[tokio::test]
2538 async fn test_bulk_insert_partial_failure() {
2539 let (collection, _temp_dir) = setup_collection().await;
2540
2541 let documents = vec![
2542 ("valid-1", json!({ "name": "One" })),
2543 ("valid-2", json!({ "name": "Two" })),
2544 ("invalid id!", json!({ "name": "Three" })), ];
2546
2547 let result = collection.bulk_insert(documents).await;
2548 assert!(result.is_err());
2549
2550 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
2552 assert!(docs.len() <= 2);
2553 }
2554
2555 #[tokio::test]
2556 async fn test_query_empty_filter() {
2557 let (collection, _temp_dir) = setup_collection().await;
2558
2559 for i in 0 .. 10 {
2560 let doc = json!({ "id": i, "value": i * 10 });
2561 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2562 }
2563
2564 let query = crate::QueryBuilder::new().build();
2565 let result = collection.query(query).await.unwrap();
2566 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2567 assert_eq!(docs.len(), 10);
2568 }
2569
2570 #[tokio::test]
2571 async fn test_query_with_limit() {
2572 let (collection, _temp_dir) = setup_collection().await;
2573
2574 for i in 0 .. 100 {
2575 let doc = json!({ "id": i });
2576 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2577 }
2578
2579 let query = crate::QueryBuilder::new().limit(5).build();
2580 let result = collection.query(query).await.unwrap();
2581 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2582 assert_eq!(docs.len(), 5);
2583 }
2584
2585 #[tokio::test]
2586 async fn test_query_with_offset() {
2587 let (collection, _temp_dir) = setup_collection().await;
2588
2589 for i in 0 .. 10 {
2590 let doc = json!({ "id": i, "value": i });
2591 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2592 }
2593
2594 let query = crate::QueryBuilder::new().offset(5).build();
2595 let result = collection.query(query).await.unwrap();
2596 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2597 assert_eq!(docs.len(), 5);
2598 }
2599
2600 #[tokio::test]
2601 async fn test_query_with_limit_and_offset() {
2602 let (collection, _temp_dir) = setup_collection().await;
2603
2604 for i in 0 .. 100 {
2605 let doc = json!({ "id": i, "value": i });
2606 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2607 }
2608
2609 let query = crate::QueryBuilder::new().offset(10).limit(5).build();
2610 let result = collection.query(query).await.unwrap();
2611 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2612 assert_eq!(docs.len(), 5);
2613 }
2614
2615 #[tokio::test]
2616 async fn test_query_with_sort_ascending() {
2617 let (collection, _temp_dir) = setup_collection().await;
2618
2619 for i in (0 .. 5).rev() {
2620 let doc = json!({ "id": i, "value": i });
2621 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2622 }
2623
2624 let query = crate::QueryBuilder::new()
2625 .sort("id", crate::SortOrder::Ascending)
2626 .build();
2627 let result = collection.query(query).await.unwrap();
2628 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2629
2630 assert_eq!(docs.len(), 5);
2631 for (i, doc) in docs.iter().enumerate() {
2632 assert_eq!(doc.data()["id"], json!(i));
2633 }
2634 }
2635
2636 #[tokio::test]
2637 async fn test_query_with_sort_descending() {
2638 let (collection, _temp_dir) = setup_collection().await;
2639
2640 for i in 0 .. 5 {
2641 let doc = json!({ "id": i, "value": i });
2642 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2643 }
2644
2645 let query = crate::QueryBuilder::new()
2646 .sort("id", crate::SortOrder::Descending)
2647 .build();
2648 let result = collection.query(query).await.unwrap();
2649 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2650
2651 assert_eq!(docs.len(), 5);
2652 for (i, doc) in docs.iter().enumerate() {
2653 assert_eq!(doc.data()["id"], json!(4 - i));
2654 }
2655 }
2656
2657 #[tokio::test]
2658 async fn test_query_with_projection() {
2659 let (collection, _temp_dir) = setup_collection().await;
2660
2661 for i in 0 .. 3 {
2662 let doc =
2663 json!({ "id": i, "name": format!("User{}", i), "email": format!("user{}@example.com", i), "age": 30 });
2664 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
2665 }
2666
2667 let query = crate::QueryBuilder::new()
2668 .projection(vec!["id", "name"])
2669 .build();
2670 let result = collection.query(query).await.unwrap();
2671 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2672
2673 assert_eq!(docs.len(), 3);
2674 for doc in &docs {
2675 assert!(doc.data().get("id").is_some());
2676 assert!(doc.data().get("name").is_some());
2677 assert!(doc.data().get("email").is_none());
2678 assert!(doc.data().get("age").is_none());
2679 }
2680 }
2681
2682 #[tokio::test]
2683 async fn test_query_with_verification() {
2684 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2685
2686 for i in 0 .. 5 {
2687 let doc = json!({ "id": i, "active": true });
2688 collection
2689 .insert(&format!("signed-{}", i), doc)
2690 .await
2691 .unwrap();
2692 }
2693
2694 let options = crate::VerificationOptions::strict();
2695 let query = crate::QueryBuilder::new().build();
2696 let result = collection
2697 .query_with_verification(query, &options)
2698 .await
2699 .unwrap();
2700 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2701 assert_eq!(docs.len(), 5);
2702 }
2703
2704 #[tokio::test]
2705 async fn test_query_complex() {
2706 let (collection, _temp_dir) = setup_collection().await;
2707
2708 let test_data = vec![
2710 (
2711 "doc1",
2712 json!({ "name": "Alice", "age": 25, "city": "NYC", "active": true }),
2713 ),
2714 (
2715 "doc2",
2716 json!({ "name": "Bob", "age": 30, "city": "LA", "active": true }),
2717 ),
2718 (
2719 "doc3",
2720 json!({ "name": "Charlie", "age": 35, "city": "NYC", "active": false }),
2721 ),
2722 (
2723 "doc4",
2724 json!({ "name": "Diana", "age": 28, "city": "NYC", "active": true }),
2725 ),
2726 (
2727 "doc5",
2728 json!({ "name": "Eve", "age": 40, "city": "LA", "active": false }),
2729 ),
2730 ];
2731
2732 for (id, doc) in &test_data {
2733 collection.insert(id, doc.clone()).await.unwrap();
2734 }
2735
2736 let query = crate::QueryBuilder::new()
2738 .filter("active", crate::Operator::Equals, json!(true))
2739 .filter("city", crate::Operator::Equals, json!("NYC"))
2740 .filter("age", crate::Operator::GreaterOrEqual, json!(26))
2741 .sort("age", crate::SortOrder::Ascending)
2742 .limit(2)
2743 .projection(vec!["name", "age"])
2744 .build();
2745
2746 let result = collection.query(query).await.unwrap();
2747 let docs: Vec<_> = result.documents.try_collect().await.unwrap();
2748
2749 assert_eq!(docs.len(), 1);
2750 assert_eq!(docs[0].data()["name"], json!("Diana"));
2752 }
2753
2754 #[tokio::test]
2755 async fn test_delete_and_recover() {
2756 let (collection, _temp_dir) = setup_collection().await;
2757
2758 let doc = json!({ "name": "ToDelete" });
2759 collection.insert("test-doc", doc.clone()).await.unwrap();
2760
2761 assert!(collection.get("test-doc").await.unwrap().is_some());
2763
2764 collection.delete("test-doc").await.unwrap();
2766
2767 assert!(collection.get("test-doc").await.unwrap().is_none());
2769
2770 let deleted_path = collection.path.join(".deleted").join("test-doc.json");
2772 assert!(tokio_fs::try_exists(&deleted_path).await.unwrap());
2773
2774 tokio_fs::rename(&deleted_path, collection.path.join("test-doc.json"))
2776 .await
2777 .unwrap();
2778
2779 assert!(collection.get("test-doc").await.unwrap().is_some());
2781 }
2782
2783 #[tokio::test]
2784 async fn test_insert_special_characters_in_data() {
2785 let (collection, _temp_dir) = setup_collection().await;
2786
2787 let doc = json!({
2788 "string": "hello\nworld\ttab",
2789 "unicode": "Hello 世界 🌍",
2790 "null": null,
2791 "array": [1, 2, 3, "four"],
2792 "nested": { "deep": { "value": 42 } }
2793 });
2794
2795 collection.insert("special-doc", doc.clone()).await.unwrap();
2796
2797 let retrieved = collection.get("special-doc").await.unwrap().unwrap();
2798 assert_eq!(retrieved.data(), &doc);
2799 }
2800
2801 #[tokio::test]
2802 async fn test_insert_very_long_document_id() {
2803 let (collection, _temp_dir) = setup_collection().await;
2804
2805 let long_id = "a".repeat(200);
2808 let doc = json!({ "data": "test" });
2809
2810 let result = collection.insert(&long_id, doc).await;
2811 assert!(result.is_ok());
2812
2813 let retrieved = collection.get(&long_id).await.unwrap().unwrap();
2814 assert_eq!(retrieved.id(), &long_id);
2815 }
2816
2817 #[tokio::test]
2818 async fn test_insert_max_value_numbers() {
2819 let (collection, _temp_dir) = setup_collection().await;
2820
2821 let doc = json!({
2822 "max_i64": 9223372036854775807i64,
2823 "min_i64": -9223372036854775808i64,
2824 "max_f64": 1.7976931348623157e308,
2825 "min_f64": -1.7976931348623157e308
2826 });
2827
2828 collection.insert("numbers", doc.clone()).await.unwrap();
2829
2830 let retrieved = collection.get("numbers").await.unwrap().unwrap();
2831 assert_eq!(retrieved.data(), &doc);
2832 }
2833
2834 #[tokio::test]
2835 async fn test_insert_nested_array_document() {
2836 let (collection, _temp_dir) = setup_collection().await;
2837
2838 let doc = json!({
2839 "matrix": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
2840 "mixed": [1, "two", true, null, { "nested": "value" }]
2841 });
2842
2843 collection.insert("arrays", doc.clone()).await.unwrap();
2844
2845 let retrieved = collection.get("arrays").await.unwrap().unwrap();
2846 assert_eq!(retrieved.data(), &doc);
2847 }
2848
2849 #[tokio::test]
2850 async fn test_collection_name() {
2851 let (collection, _temp_dir) = setup_collection().await;
2852
2853 assert_eq!(collection.name(), "test_collection");
2854 }
2855
2856 #[tokio::test]
2857 async fn test_verify_hash_valid() {
2858 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2859
2860 let doc = json!({ "name": "Test" });
2861 collection.insert("hash-test", doc.clone()).await.unwrap();
2862
2863 let retrieved = collection.get("hash-test").await.unwrap().unwrap();
2864 let options = crate::VerificationOptions {
2865 verify_signature: false,
2866 verify_hash: true,
2867 signature_verification_mode: crate::VerificationMode::Strict,
2868 empty_signature_mode: crate::VerificationMode::Warn,
2869 hash_verification_mode: crate::VerificationMode::Strict,
2870 };
2871
2872 let result = collection.verify_document(&retrieved, &options).await;
2873 assert!(result.is_ok());
2874 }
2875
2876 #[tokio::test]
2877 async fn test_verify_hash_invalid() {
2878 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2879
2880 let doc = json!({ "name": "Original" });
2881 collection
2882 .insert("hash-invalid", doc.clone())
2883 .await
2884 .unwrap();
2885
2886 let file_path = collection.path.join("hash-invalid.json");
2888 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2889 content = content.replace("Original", "Tampered");
2890 tokio_fs::write(&file_path, &content).await.unwrap();
2891
2892 let retrieved = collection
2894 .get_with_verification("hash-invalid", &crate::VerificationOptions::disabled())
2895 .await
2896 .unwrap()
2897 .unwrap();
2898
2899 let options = crate::VerificationOptions {
2900 verify_signature: false,
2901 verify_hash: true,
2902 signature_verification_mode: crate::VerificationMode::Strict,
2903 empty_signature_mode: crate::VerificationMode::Warn,
2904 hash_verification_mode: crate::VerificationMode::Strict,
2905 };
2906
2907 let result = collection.verify_document(&retrieved, &options).await;
2908 assert!(result.is_err());
2909 match result.unwrap_err() {
2910 crate::SentinelError::HashVerificationFailed {
2911 id,
2912 ..
2913 } => {
2914 assert_eq!(id, "hash-invalid");
2915 },
2916 _ => panic!("Expected HashVerificationFailed"),
2917 }
2918 }
2919
2920 #[tokio::test]
2921 async fn test_verify_signature_valid() {
2922 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2923
2924 let doc = json!({ "name": "Signed" });
2925 collection
2926 .insert("signed-valid", doc.clone())
2927 .await
2928 .unwrap();
2929
2930 let retrieved = collection.get("signed-valid").await.unwrap().unwrap();
2931 let options = crate::VerificationOptions::strict();
2932
2933 let result = collection.verify_document(&retrieved, &options).await;
2934 assert!(result.is_ok());
2935 }
2936
2937 #[tokio::test]
2938 async fn test_verify_signature_invalid() {
2939 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
2940
2941 let doc = json!({ "name": "Original" });
2942 collection.insert("sig-invalid", doc.clone()).await.unwrap();
2943
2944 let file_path = collection.path.join("sig-invalid.json");
2946 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
2947 content = content.replace("Original", "Tampered");
2948 tokio_fs::write(&file_path, &content).await.unwrap();
2949
2950 let retrieved = collection
2952 .get_with_verification("sig-invalid", &crate::VerificationOptions::disabled())
2953 .await
2954 .unwrap()
2955 .unwrap();
2956 let options = crate::VerificationOptions::strict();
2957
2958 let result = collection.verify_document(&retrieved, &options).await;
2959 assert!(result.is_err());
2960 }
2961
2962 #[tokio::test]
2963 async fn test_insert_unsigned_document() {
2964 let temp_dir = tempfile::tempdir().unwrap();
2966 let store = Store::new(temp_dir.path(), None).await.unwrap();
2967 let collection = store.collection("test").await.unwrap();
2968
2969 let data = json!({ "name": "test" });
2970 let result = collection.insert("unsigned-doc", data).await;
2971 assert!(result.is_ok());
2972
2973 let doc = collection.get("unsigned-doc").await.unwrap().unwrap();
2974 assert_eq!(doc.data()["name"], "test");
2975 }
2976
2977 #[tokio::test]
2978 async fn test_delete_nonexistent_document() {
2979 let (collection, _temp_dir) = setup_collection().await;
2981
2982 let result = collection.delete("nonexistent-doc").await;
2984 assert!(result.is_ok()); }
2986
2987 #[tokio::test]
2988 async fn test_delete_soft_delete_path() {
2989 let (collection, temp_dir) = setup_collection().await;
2991
2992 let data = json!({ "name": "to-delete" });
2994 collection.insert("doc-to-delete", data).await.unwrap();
2995
2996 let result = collection.delete("doc-to-delete").await;
2998 assert!(result.is_ok());
2999
3000 let deleted_path = temp_dir
3002 .path()
3003 .join("data/test_collection/.deleted/doc-to-delete.json");
3004 assert!(tokio::fs::metadata(&deleted_path).await.is_ok());
3005 }
3006
3007 #[tokio::test]
3008 async fn test_streaming_all_skips_deleted() {
3009 let (collection, _temp_dir) = setup_collection().await;
3010
3011 for i in 0 .. 5 {
3012 let doc = json!({ "id": i });
3013 collection.insert(&format!("doc-{}", i), doc).await.unwrap();
3014 }
3015
3016 collection.delete("doc-1").await.unwrap();
3018 collection.delete("doc-3").await.unwrap();
3019
3020 let docs: Vec<_> = collection.all().try_collect().await.unwrap();
3021 assert_eq!(docs.len(), 3);
3022
3023 let ids: std::collections::HashSet<_> = docs.iter().map(|d| d.id().to_string()).collect();
3024 assert!(ids.contains("doc-0"));
3025 assert!(!ids.contains("doc-1"));
3026 assert!(ids.contains("doc-2"));
3027 assert!(!ids.contains("doc-3"));
3028 assert!(ids.contains("doc-4"));
3029 }
3030
3031 #[tokio::test]
3032 async fn test_count_method() {
3033 let (collection, _temp_dir) = setup_collection().await;
3035
3036 collection
3037 .insert("doc-1", json!({"data": 1}))
3038 .await
3039 .unwrap();
3040 collection
3041 .insert("doc-2", json!({"data": 2}))
3042 .await
3043 .unwrap();
3044
3045 let count = collection.count().await.unwrap();
3046 assert_eq!(count, 2);
3047 }
3048
3049 #[tokio::test]
3050 async fn test_get_many() {
3051 let (collection, _temp_dir) = setup_collection().await;
3053
3054 collection.insert("doc-1", json!({"id": 1})).await.unwrap();
3055 collection.insert("doc-2", json!({"id": 2})).await.unwrap();
3056
3057 let ids = vec!["doc-1", "doc-2", "non-existent"];
3058 let results = collection.get_many(&ids).await.unwrap();
3059
3060 assert_eq!(results.len(), 3);
3061 assert!(results[0].is_some());
3062 assert!(results[1].is_some());
3063 assert!(results[2].is_none());
3064 }
3065
3066 #[tokio::test]
3067 async fn test_upsert_insert() {
3068 let (collection, _temp_dir) = setup_collection().await;
3070
3071 let is_new = collection
3072 .upsert("new-doc", json!({"value": 100}))
3073 .await
3074 .unwrap();
3075 assert!(is_new);
3076
3077 let doc = collection.get("new-doc").await.unwrap().unwrap();
3078 assert_eq!(doc.data().get("value").unwrap(), &json!(100));
3079 }
3080
3081 #[tokio::test]
3082 async fn test_upsert_update() {
3083 let (collection, _temp_dir) = setup_collection().await;
3085
3086 collection
3087 .insert("existing", json!({"value": 1}))
3088 .await
3089 .unwrap();
3090 let is_new = collection
3091 .upsert("existing", json!({"value": 2}))
3092 .await
3093 .unwrap();
3094
3095 assert!(!is_new);
3096 let doc = collection.get("existing").await.unwrap().unwrap();
3097 assert_eq!(doc.data().get("value").unwrap(), &json!(2));
3098 }
3099
3100 #[tokio::test]
3101 async fn test_aggregate_count() {
3102 let (collection, _temp_dir) = setup_collection().await;
3104
3105 collection
3106 .insert("doc-1", json!({"value": 1}))
3107 .await
3108 .unwrap();
3109 collection
3110 .insert("doc-2", json!({"value": 2}))
3111 .await
3112 .unwrap();
3113
3114 let result = collection
3115 .aggregate(vec![], crate::Aggregation::Count)
3116 .await
3117 .unwrap();
3118 assert_eq!(result, json!(2));
3119 }
3120
3121 #[tokio::test]
3122 async fn test_aggregate_sum() {
3123 let (collection, _temp_dir) = setup_collection().await;
3125
3126 collection
3127 .insert("doc-1", json!({"amount": 10}))
3128 .await
3129 .unwrap();
3130 collection
3131 .insert("doc-2", json!({"amount": 20}))
3132 .await
3133 .unwrap();
3134
3135 let result = collection
3136 .aggregate(vec![], crate::Aggregation::Sum("amount".to_string()))
3137 .await
3138 .unwrap();
3139 assert_eq!(result, json!(30.0));
3140 }
3141
3142 #[tokio::test]
3143 async fn test_aggregate_avg() {
3144 let (collection, _temp_dir) = setup_collection().await;
3146
3147 collection
3148 .insert("doc-1", json!({"score": 10}))
3149 .await
3150 .unwrap();
3151 collection
3152 .insert("doc-2", json!({"score": 20}))
3153 .await
3154 .unwrap();
3155 collection
3156 .insert("doc-3", json!({"score": 30}))
3157 .await
3158 .unwrap();
3159
3160 let result = collection
3161 .aggregate(vec![], crate::Aggregation::Avg("score".to_string()))
3162 .await
3163 .unwrap();
3164 assert_eq!(result, json!(20.0));
3165 }
3166
3167 #[tokio::test]
3168 async fn test_aggregate_avg_no_docs() {
3169 let (collection, _temp_dir) = setup_collection().await;
3171
3172 let result = collection
3173 .aggregate(vec![], crate::Aggregation::Avg("score".to_string()))
3174 .await
3175 .unwrap();
3176 assert_eq!(result, json!(null));
3177 }
3178
3179 #[tokio::test]
3180 async fn test_aggregate_min() {
3181 let (collection, _temp_dir) = setup_collection().await;
3183
3184 collection
3185 .insert("doc-1", json!({"value": 15}))
3186 .await
3187 .unwrap();
3188 collection
3189 .insert("doc-2", json!({"value": 5}))
3190 .await
3191 .unwrap();
3192 collection
3193 .insert("doc-3", json!({"value": 10}))
3194 .await
3195 .unwrap();
3196
3197 let result = collection
3198 .aggregate(vec![], crate::Aggregation::Min("value".to_string()))
3199 .await
3200 .unwrap();
3201 assert_eq!(result, json!(5.0));
3202 }
3203
3204 #[tokio::test]
3205 async fn test_aggregate_min_no_values() {
3206 let (collection, _temp_dir) = setup_collection().await;
3208
3209 collection
3210 .insert("doc-1", json!({"name": "test"}))
3211 .await
3212 .unwrap();
3213
3214 let result = collection
3215 .aggregate(vec![], crate::Aggregation::Min("value".to_string()))
3216 .await
3217 .unwrap();
3218 assert_eq!(result, json!(null));
3219 }
3220
3221 #[tokio::test]
3222 async fn test_aggregate_max() {
3223 let (collection, _temp_dir) = setup_collection().await;
3225
3226 collection
3227 .insert("doc-1", json!({"value": 15}))
3228 .await
3229 .unwrap();
3230 collection
3231 .insert("doc-2", json!({"value": 25}))
3232 .await
3233 .unwrap();
3234 collection
3235 .insert("doc-3", json!({"value": 10}))
3236 .await
3237 .unwrap();
3238
3239 let result = collection
3240 .aggregate(vec![], crate::Aggregation::Max("value".to_string()))
3241 .await
3242 .unwrap();
3243 assert_eq!(result, json!(25.0));
3244 }
3245
3246 #[tokio::test]
3247 async fn test_aggregate_max_no_values() {
3248 let (collection, _temp_dir) = setup_collection().await;
3250
3251 let result = collection
3252 .aggregate(vec![], crate::Aggregation::Max("value".to_string()))
3253 .await
3254 .unwrap();
3255 assert_eq!(result, json!(null));
3256 }
3257
3258 #[tokio::test]
3259 async fn test_aggregate_with_filters() {
3260 let (collection, _temp_dir) = setup_collection().await;
3262
3263 collection
3264 .insert("doc-1", json!({"category": "A", "value": 10}))
3265 .await
3266 .unwrap();
3267 collection
3268 .insert("doc-2", json!({"category": "B", "value": 20}))
3269 .await
3270 .unwrap();
3271 collection
3272 .insert("doc-3", json!({"category": "A", "value": 15}))
3273 .await
3274 .unwrap();
3275
3276 let filters = vec![crate::Filter::Equals("category".to_string(), json!("A"))];
3277 let result = collection
3278 .aggregate(filters, crate::Aggregation::Sum("value".to_string()))
3279 .await
3280 .unwrap();
3281 assert_eq!(result, json!(25.0));
3282 }
3283
3284 #[tokio::test]
3285 async fn test_update_not_found() {
3286 let (collection, _temp_dir) = setup_collection().await;
3288
3289 let result = collection
3290 .update("non-existent", json!({"data": "value"}))
3291 .await;
3292 assert!(matches!(
3293 result,
3294 Err(crate::SentinelError::DocumentNotFound { .. })
3295 ));
3296 }
3297
3298 #[tokio::test]
3299 async fn test_update_merge_json_non_object() {
3300 let (collection, _temp_dir) = setup_collection().await;
3302
3303 collection
3304 .insert("doc-1", json!({"name": "old"}))
3305 .await
3306 .unwrap();
3307 collection
3308 .update("doc-1", json!("simple string"))
3309 .await
3310 .unwrap();
3311
3312 let doc = collection.get("doc-1").await.unwrap().unwrap();
3313 assert_eq!(doc.data(), &json!("simple string"));
3314 }
3315
3316 #[tokio::test]
3317 async fn test_extract_numeric_value() {
3318 let (collection, _temp_dir) = setup_collection().await;
3320
3321 collection
3322 .insert("doc-1", json!({"price": 99.99, "name": "Product"}))
3323 .await
3324 .unwrap();
3325 let doc = collection.get("doc-1").await.unwrap().unwrap();
3326
3327 let price = Collection::extract_numeric_value(&doc, "price");
3328 assert_eq!(price, Some(99.99));
3329
3330 let name = Collection::extract_numeric_value(&doc, "name");
3331 assert_eq!(name, None);
3332
3333 let missing = Collection::extract_numeric_value(&doc, "missing_field");
3334 assert_eq!(missing, None);
3335 }
3336
3337 #[tokio::test]
3338 async fn test_delete_non_existent() {
3339 let (collection, _temp_dir) = setup_collection().await;
3341
3342 let result = collection.delete("does-not-exist").await;
3344 assert!(result.is_ok());
3345 }
3346
3347 #[tokio::test]
3348 async fn test_update_unsigned_document() {
3349 let temp_dir = tempdir().unwrap();
3351 let store = Store::new(temp_dir.path(), None).await.unwrap();
3352 let collection = store.collection("test").await.unwrap();
3353
3354 collection
3355 .insert("doc-1", json!({"count": 1}))
3356 .await
3357 .unwrap();
3358 collection
3359 .update("doc-1", json!({"count": 2}))
3360 .await
3361 .unwrap();
3362
3363 let doc = collection.get("doc-1").await.unwrap().unwrap();
3364 assert_eq!(doc.data().get("count").unwrap(), &json!(2));
3365 assert_eq!(doc.signature(), ""); }
3367
3368 #[tokio::test]
3369 async fn test_filter_with_malformed_json() {
3370 use tokio::fs as tokio_fs;
3372 let (collection, _temp_dir) = setup_collection().await;
3373
3374 collection
3376 .insert("valid-doc", json!({"data": "valid"}))
3377 .await
3378 .unwrap();
3379
3380 let malformed_path = collection.path.join("malformed.json");
3382 tokio_fs::write(&malformed_path, "{ this is not valid json }")
3383 .await
3384 .unwrap();
3385
3386 let mut stream = collection.filter(|_| true);
3388 let mut found_valid = false;
3389 let mut found_error = false;
3390
3391 while let Some(result) = stream.next().await {
3392 match result {
3393 Ok(doc) if doc.id() == "valid-doc" => found_valid = true,
3394 Err(_) => found_error = true,
3395 _ => {},
3396 }
3397 }
3398
3399 assert!(found_valid);
3400 assert!(found_error); }
3402
3403 #[tokio::test]
3404 async fn test_all_with_malformed_json() {
3405 use tokio::fs as tokio_fs;
3407 let (collection, _temp_dir) = setup_collection().await;
3408
3409 collection
3410 .insert("doc-1", json!({"data": "valid"}))
3411 .await
3412 .unwrap();
3413
3414 let bad_path = collection.path.join("bad.json");
3416 tokio_fs::write(&bad_path, "not json at all").await.unwrap();
3417
3418 let mut stream = collection.all();
3419 let mut found_valid = false;
3420 let mut found_error = false;
3421
3422 while let Some(result) = stream.next().await {
3423 match result {
3424 Ok(doc) if doc.id() == "doc-1" => found_valid = true,
3425 Err(_) => found_error = true,
3426 _ => {},
3427 }
3428 }
3429
3430 assert!(found_valid);
3431 assert!(found_error);
3432 }
3433
3434 #[tokio::test]
3435 async fn test_query_with_malformed_json() {
3436 use tokio::fs as tokio_fs;
3438 let (collection, _temp_dir) = setup_collection().await;
3439
3440 collection
3441 .insert("valid", json!({"value": 42}))
3442 .await
3443 .unwrap();
3444
3445 let invalid_path = collection.path.join("invalid.json");
3447 tokio_fs::write(&invalid_path, "{broken json}")
3448 .await
3449 .unwrap();
3450
3451 let query = crate::QueryBuilder::new().build();
3452 let result = collection.query(query).await.unwrap();
3453
3454 let mut stream = result.documents;
3455 let mut found_valid = false;
3456 let mut found_error = false;
3457
3458 while let Some(result) = stream.next().await {
3459 match result {
3460 Ok(doc) if doc.id() == "valid" => found_valid = true,
3461 Err(_) => found_error = true,
3462 _ => {},
3463 }
3464 }
3465
3466 assert!(found_valid);
3467 assert!(found_error);
3468 }
3469
3470 #[tokio::test]
3471 async fn test_filter_with_strict_hash_verification_failure() {
3472 use tokio::fs as tokio_fs;
3474 let (collection, _temp_dir) = setup_collection().await;
3475
3476 collection
3477 .insert("doc-1", json!({"data": "test"}))
3478 .await
3479 .unwrap();
3480
3481 let file_path = collection.path.join("doc-1.json");
3483 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3484 content = content.replace("\"hash\":", "\"hash\": \"corrupted_hash\", \"old_hash\":");
3485 tokio_fs::write(&file_path, content).await.unwrap();
3486
3487 let options = crate::VerificationOptions {
3488 verify_hash: true,
3489 hash_verification_mode: crate::VerificationMode::Strict,
3490 ..Default::default()
3491 };
3492
3493 let results: Result<Vec<_>> = collection
3494 .filter_with_verification(|_| true, &options)
3495 .try_collect()
3496 .await;
3497 assert!(results.is_err()); }
3499
3500 #[tokio::test]
3501 async fn test_all_with_strict_verification_failure() {
3502 use tokio::fs as tokio_fs;
3504 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3505
3506 collection
3507 .insert("doc-1", json!({"data": "test"}))
3508 .await
3509 .unwrap();
3510
3511 let file_path = collection.path.join("doc-1.json");
3513 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3514 content = content.replace(
3515 "\"signature\":",
3516 "\"signature\": \"bad_sig\", \"original_signature\":",
3517 );
3518 tokio_fs::write(&file_path, content).await.unwrap();
3519
3520 let options = crate::VerificationOptions {
3521 verify_signature: true,
3522 signature_verification_mode: crate::VerificationMode::Strict,
3523 ..Default::default()
3524 };
3525
3526 let results: Result<Vec<_>> = collection
3527 .all_with_verification(&options)
3528 .try_collect()
3529 .await;
3530 assert!(results.is_err());
3531 }
3532
3533 #[tokio::test]
3534 async fn test_query_with_strict_verification_failure() {
3535 use tokio::fs as tokio_fs;
3537 let (collection, _temp_dir) = setup_collection().await;
3538
3539 collection
3540 .insert("doc-1", json!({"value": 42}))
3541 .await
3542 .unwrap();
3543
3544 let file_path = collection.path.join("doc-1.json");
3546 let mut content = tokio_fs::read_to_string(&file_path).await.unwrap();
3547 content = content.replace("\"hash\":", "\"hash\": \"invalid_hash\", \"real_hash\":");
3548 tokio_fs::write(&file_path, content).await.unwrap();
3549
3550 let options = crate::VerificationOptions {
3551 verify_hash: true,
3552 hash_verification_mode: crate::VerificationMode::Strict,
3553 ..Default::default()
3554 };
3555
3556 let query = crate::QueryBuilder::new().build();
3557 let result = collection
3558 .query_with_verification(query, &options)
3559 .await
3560 .unwrap();
3561 let results: Result<Vec<_>> = result.documents.try_collect().await;
3562 assert!(results.is_err());
3563 }
3564
3565 #[tokio::test]
3566 async fn test_verify_hash_silent_mode() {
3567 let (collection, _temp_dir) = setup_collection().await;
3569
3570 collection
3571 .insert("doc-1", json!({"data": "test"}))
3572 .await
3573 .unwrap();
3574 let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3575 doc.hash = "corrupted".to_string(); let options = crate::VerificationOptions {
3578 verify_hash: true,
3579 hash_verification_mode: crate::VerificationMode::Silent,
3580 ..Default::default()
3581 };
3582
3583 let result = collection.verify_hash(&doc, options).await;
3584 assert!(result.is_ok()); }
3586
3587 #[tokio::test]
3588 async fn test_verify_hash_warn_mode_invalid() {
3589 let (collection, _temp_dir) = setup_collection().await;
3591
3592 collection
3593 .insert("doc-1", json!({"data": "test"}))
3594 .await
3595 .unwrap();
3596 let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3597 doc.hash = "definitely_wrong".to_string();
3598
3599 let options = crate::VerificationOptions {
3600 verify_hash: true,
3601 hash_verification_mode: crate::VerificationMode::Warn,
3602 ..Default::default()
3603 };
3604
3605 let result = collection.verify_hash(&doc, options).await;
3606 assert!(result.is_ok()); }
3608
3609 #[tokio::test]
3610 async fn test_verify_hash_strict_mode_failure() {
3611 let (collection, _temp_dir) = setup_collection().await;
3613
3614 collection
3615 .insert("doc-1", json!({"data": "test"}))
3616 .await
3617 .unwrap();
3618 let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3619 doc.hash = "wrong_hash".to_string();
3620
3621 let options = crate::VerificationOptions {
3622 verify_hash: true,
3623 hash_verification_mode: crate::VerificationMode::Strict,
3624 ..Default::default()
3625 };
3626
3627 let result = collection.verify_hash(&doc, options).await;
3628 assert!(matches!(
3629 result,
3630 Err(crate::SentinelError::HashVerificationFailed { .. })
3631 ));
3632 }
3633
3634 #[tokio::test]
3635 async fn test_verify_signature_disabled() {
3636 let (collection, _temp_dir) = setup_collection().await;
3638
3639 collection
3640 .insert("doc-1", json!({"data": "test"}))
3641 .await
3642 .unwrap();
3643 let doc = collection.get("doc-1").await.unwrap().unwrap();
3644
3645 let options = crate::VerificationOptions {
3646 verify_signature: false,
3647 ..Default::default()
3648 };
3649
3650 let result = collection.verify_signature(&doc, options).await;
3651 assert!(result.is_ok());
3652 }
3653
3654 #[tokio::test]
3655 async fn test_verify_signature_strict_mode_failure() {
3656 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3658
3659 collection
3660 .insert("doc-1", json!({"data": "test"}))
3661 .await
3662 .unwrap();
3663 let mut doc = collection.get("doc-1").await.unwrap().unwrap();
3664
3665 doc.signature = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".to_string();
3667
3668 let options = crate::VerificationOptions {
3669 verify_signature: true,
3670 signature_verification_mode: crate::VerificationMode::Strict,
3671 ..Default::default()
3672 };
3673
3674 let result = collection.verify_signature(&doc, options).await;
3675 assert!(result.is_err());
3677 }
3678
3679 #[tokio::test]
3680 async fn test_verify_signature_warn_mode_invalid() {
3681 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3683
3684 collection
3685 .insert("doc-1", json!({"data": "test"}))
3686 .await
3687 .unwrap();
3688 collection
3689 .insert("doc-2", json!({"data": "different"}))
3690 .await
3691 .unwrap();
3692
3693 let mut doc1 = collection.get("doc-1").await.unwrap().unwrap();
3694 let doc2 = collection.get("doc-2").await.unwrap().unwrap();
3695
3696 doc1.signature = doc2.signature().to_string();
3698
3699 let options = crate::VerificationOptions {
3700 verify_signature: true,
3701 signature_verification_mode: crate::VerificationMode::Warn,
3702 ..Default::default()
3703 };
3704
3705 let result = collection.verify_signature(&doc1, options).await;
3706 assert!(result.is_ok());
3708 }
3709
3710 #[tokio::test]
3711 async fn test_verify_signature_silent_mode() {
3712 let (collection, _temp_dir) = setup_collection().await;
3714
3715 collection
3716 .insert("doc-1", json!({"data": "test"}))
3717 .await
3718 .unwrap();
3719 let doc = collection.get("doc-1").await.unwrap().unwrap();
3720
3721 let options = crate::VerificationOptions {
3722 verify_signature: true,
3723 signature_verification_mode: crate::VerificationMode::Silent,
3724 empty_signature_mode: crate::VerificationMode::Silent,
3725 ..Default::default()
3726 };
3727
3728 let result = collection.verify_signature(&doc, options).await;
3729 assert!(result.is_ok());
3730 }
3731
3732 #[tokio::test]
3733 async fn test_verify_signature_success() {
3734 let (collection, _temp_dir) = setup_collection_with_signing_key().await;
3736
3737 collection
3738 .insert("doc-1", json!({"data": "test"}))
3739 .await
3740 .unwrap();
3741 let doc = collection.get("doc-1").await.unwrap().unwrap();
3742
3743 let options = crate::VerificationOptions {
3744 verify_signature: true,
3745 signature_verification_mode: crate::VerificationMode::Strict,
3746 ..Default::default()
3747 };
3748
3749 let result = collection.verify_signature(&doc, options).await;
3750 assert!(result.is_ok());
3751 }
3752
3753 #[tokio::test]
3754 async fn test_bulk_insert_trace_logs() {
3755 let (collection, _temp_dir) = setup_collection().await;
3757
3758 let docs = vec![
3759 ("doc-1", json!({"value": 1})),
3760 ("doc-2", json!({"value": 2})),
3761 ];
3762
3763 collection.bulk_insert(docs).await.unwrap();
3764
3765 let doc1 = collection.get("doc-1").await.unwrap().unwrap();
3766 assert_eq!(doc1.data().get("value").unwrap(), &json!(1));
3767 }
3768
3769 #[tokio::test]
3770 async fn test_update_without_signing_key() {
3771 let temp_dir = tempfile::tempdir().unwrap();
3773
3774 let store = Store::new(temp_dir.path(), None).await.unwrap();
3776 let collection = store.collection("test_collection").await.unwrap();
3777
3778 collection
3780 .insert("doc1", json!({"name": "Alice", "age": 30}))
3781 .await
3782 .unwrap();
3783
3784 collection.update("doc1", json!({"age": 31})).await.unwrap();
3786
3787 let updated_doc = collection.get("doc1").await.unwrap().unwrap();
3789 assert_eq!(updated_doc.data()["age"], 31);
3790 assert_eq!(updated_doc.data()["name"], "Alice");
3791 }
3792
3793 #[tokio::test]
3794 async fn test_verify_signature_no_signing_key() {
3795 let temp_dir = tempfile::tempdir().unwrap();
3797
3798 let store = Store::new(temp_dir.path(), None).await.unwrap();
3800 let collection = store.collection("test_collection").await.unwrap();
3801
3802 collection
3804 .insert("doc1", json!({"name": "Alice"}))
3805 .await
3806 .unwrap();
3807
3808 let doc = collection.get("doc1").await.unwrap().unwrap();
3810 let options = crate::verification::VerificationOptions {
3811 verify_hash: true,
3812 verify_signature: true,
3813 hash_verification_mode: crate::VerificationMode::Strict,
3814 signature_verification_mode: crate::VerificationMode::Strict,
3815 empty_signature_mode: crate::VerificationMode::Warn,
3816 };
3817
3818 collection.verify_document(&doc, &options).await.unwrap();
3820 }
3821
3822 #[tokio::test]
3823 async fn test_update_with_signing_key() {
3824 let temp_dir = tempfile::tempdir().unwrap();
3826
3827 let store = Store::new(temp_dir.path(), Some("test_passphrase"))
3829 .await
3830 .unwrap();
3831 let collection = store.collection("test_collection").await.unwrap();
3832
3833 collection
3835 .insert("doc1", json!({"name": "Alice", "age": 30}))
3836 .await
3837 .unwrap();
3838
3839 collection.update("doc1", json!({"age": 31})).await.unwrap();
3841
3842 let updated_doc = collection.get("doc1").await.unwrap().unwrap();
3844 assert_eq!(updated_doc.data()["age"], 31);
3845 assert_eq!(updated_doc.data()["name"], "Alice");
3846 }
3847}