@@ -151,7 +151,7 @@ def can_write(client, cloud_config):
151151 ds_id = result .get ("_id" , result .get ("id" , "" ))
152152 if ds_id :
153153 try :
154- delete_dataset (client , ds_id )
154+ delete_dataset (client , ds_id , when = "now" )
155155 except Exception :
156156 pass
157157 return True
@@ -183,7 +183,7 @@ def fresh_dataset(client, cloud_config, can_write):
183183
184184 # Teardown: delete the dataset
185185 try :
186- delete_dataset (client , dataset_id )
186+ delete_dataset (client , dataset_id , when = "now" )
187187 except Exception :
188188 pass
189189
@@ -364,7 +364,7 @@ def test_create_and_delete_dataset(self, client, cloud_config, can_write):
364364 assert ds .get ("_id" ) == ds_id or ds .get ("id" ) == ds_id
365365 finally :
366366 try :
367- _retry_on_server_error (lambda : delete_dataset (client , ds_id ))
367+ _retry_on_server_error (lambda : delete_dataset (client , ds_id , when = "now" ))
368368 except Exception :
369369 pass # Best-effort cleanup
370370
@@ -463,7 +463,7 @@ def test_add_get_delete_document(self, client, fresh_dataset):
463463 assert fetched .get ("base" , {}).get ("name" ) == "test_document"
464464
465465 # Delete
466- delete_document (client , fresh_dataset , doc_id )
466+ delete_document (client , fresh_dataset , doc_id , when = "now" )
467467
468468 # Verify gone
469469 from ndi .cloud .exceptions import CloudAPIError
@@ -499,7 +499,7 @@ def test_update_document(self, client, fresh_dataset):
499499 assert fetched .get ("base" , {}).get ("name" ) == "modified"
500500 finally :
501501 try :
502- delete_document (client , fresh_dataset , doc_id )
502+ delete_document (client , fresh_dataset , doc_id , when = "now" )
503503 except Exception :
504504 pass
505505
@@ -617,7 +617,7 @@ def test_bulk_delete(self, client, fresh_dataset):
617617 doc_ids .append (result .get ("_id" , result .get ("id" , "" )))
618618
619619 # Delete the first 3
620- bulk_delete (client , fresh_dataset , doc_ids [:3 ])
620+ bulk_delete (client , fresh_dataset , doc_ids [:3 ], when = "now" )
621621
622622 # Small delay for server processing
623623 time .sleep (2 )
@@ -873,6 +873,194 @@ def test_unpublished_datasets_list(self, client):
873873 assert isinstance (result , dict )
874874
875875
876+ # ===========================================================================
877+ # TestSoftDelete -- soft-delete, undelete, list-deleted (requires write)
878+ # ===========================================================================
879+
880+
881+ class TestSoftDelete :
882+ """Soft-delete API: deferred delete, undelete, and list-deleted."""
883+
884+ def test_deferred_delete_and_undelete (self , client , cloud_config , can_write ):
885+ """Delete with when='7d', verify listed as deleted, then undelete.
886+
887+ Creates a dataset WITH documents to mimic real-world usage.
888+ """
889+ if not can_write :
890+ pytest .skip ("User does not have dataset creation privileges" )
891+
892+ from ndi .cloud .api .datasets import (
893+ create_dataset ,
894+ delete_dataset ,
895+ get_dataset ,
896+ list_deleted_datasets ,
897+ undelete_dataset ,
898+ )
899+ from ndi .cloud .api .documents import add_document , list_all_documents
900+ from ndi .cloud .exceptions import CloudAPIError as _APIError
901+
902+ org_id = cloud_config .org_id
903+ try :
904+ result = _retry_on_server_error (
905+ lambda : create_dataset (client , org_id , "NDI_PYTEST_SOFT_DELETE" )
906+ )
907+ except _APIError as exc :
908+ pytest .skip (f"create_dataset timed out: { exc } " )
909+ ds_id = result .get ("_id" , result .get ("id" , "" ))
910+ assert ds_id
911+
912+ try :
913+ # Add documents to make it realistic
914+ for i in range (3 ):
915+ add_document (
916+ client ,
917+ ds_id ,
918+ {
919+ "document_class" : {"class_name" : "ndi_pytest_softdel" },
920+ "base" : {"name" : f"softdel_doc_{ i } " },
921+ },
922+ )
923+
924+ # Deferred delete (7 days)
925+ del_result = delete_dataset (client , ds_id , when = "7d" )
926+ assert isinstance (del_result , dict )
927+ assert "message" in del_result
928+
929+ # Should appear in deleted list
930+ time .sleep (2 )
931+ deleted = list_deleted_datasets (client )
932+ deleted_ids = {d .get ("_id" , d .get ("id" , "" )) for d in deleted .get ("datasets" , [])}
933+ assert ds_id in deleted_ids , f"Dataset { ds_id } not found in deleted list"
934+
935+ # Undelete
936+ undelete_result = undelete_dataset (client , ds_id )
937+ assert isinstance (undelete_result , dict )
938+
939+ # Should be accessible again with documents intact
940+ time .sleep (2 )
941+ ds = _retry_on_server_error (lambda : get_dataset (client , ds_id ), retry_on_404 = True )
942+ ds_fetched_id = ds .get ("_id" , ds .get ("id" , "" ))
943+ assert ds_fetched_id == ds_id
944+
945+ # Verify documents survived the soft-delete round-trip
946+ docs = list_all_documents (client , ds_id )
947+ assert len (docs ) >= 3 , f"Expected >= 3 docs after undelete, got { len (docs )} "
948+ finally :
949+ # Final cleanup
950+ try :
951+ delete_dataset (client , ds_id , when = "now" )
952+ except Exception :
953+ pass
954+
955+ def test_immediate_delete_cannot_undelete (self , client , cloud_config , can_write ):
956+ """Delete with when='now' — undelete 10s later should fail.
957+
958+ Creates a dataset WITH documents to mimic real-world usage.
959+ """
960+ if not can_write :
961+ pytest .skip ("User does not have dataset creation privileges" )
962+
963+ from ndi .cloud .api .datasets import (
964+ create_dataset ,
965+ delete_dataset ,
966+ undelete_dataset ,
967+ )
968+ from ndi .cloud .api .documents import add_document
969+ from ndi .cloud .exceptions import CloudAPIError as _APIError
970+
971+ org_id = cloud_config .org_id
972+ try :
973+ result = _retry_on_server_error (
974+ lambda : create_dataset (client , org_id , "NDI_PYTEST_HARD_DELETE" )
975+ )
976+ except _APIError as exc :
977+ pytest .skip (f"create_dataset timed out: { exc } " )
978+ ds_id = result .get ("_id" , result .get ("id" , "" ))
979+ assert ds_id
980+
981+ # Add documents to make it realistic
982+ for i in range (3 ):
983+ add_document (
984+ client ,
985+ ds_id ,
986+ {
987+ "document_class" : {"class_name" : "ndi_pytest_harddel" },
988+ "base" : {"name" : f"harddel_doc_{ i } " },
989+ },
990+ )
991+
992+ # Immediate delete
993+ delete_dataset (client , ds_id , when = "now" )
994+ time .sleep (10 )
995+
996+ # Undelete should fail — dataset is permanently gone
997+ with pytest .raises (_APIError ):
998+ undelete_dataset (client , ds_id )
999+
1000+ def test_list_deleted_documents (self , client , fresh_dataset ):
1001+ """Add doc, delete it, verify it appears in deleted-documents list."""
1002+ from ndi .cloud .api .documents import (
1003+ add_document ,
1004+ delete_document ,
1005+ list_deleted_documents ,
1006+ )
1007+
1008+ doc_json = {
1009+ "document_class" : {"class_name" : "ndi_pytest_softdel" },
1010+ "base" : {"name" : "soft_delete_test" },
1011+ }
1012+ result = add_document (client , fresh_dataset , doc_json )
1013+ doc_id = result .get ("_id" , result .get ("id" , "" ))
1014+ assert doc_id
1015+
1016+ delete_document (client , fresh_dataset , doc_id , when = "now" )
1017+ time .sleep (2 )
1018+
1019+ deleted = list_deleted_documents (client , fresh_dataset )
1020+ assert isinstance (deleted , dict )
1021+ # The response should have a documents list
1022+ deleted_docs = deleted .get ("documents" , [])
1023+ assert isinstance (deleted_docs , list )
1024+
1025+ def test_delete_dataset_returns_message (self , client , cloud_config , can_write ):
1026+ """delete_dataset should return a response dict with a message."""
1027+ if not can_write :
1028+ pytest .skip ("User does not have dataset creation privileges" )
1029+
1030+ from ndi .cloud .api .datasets import create_dataset , delete_dataset
1031+ from ndi .cloud .exceptions import CloudAPIError as _APIError
1032+
1033+ org_id = cloud_config .org_id
1034+ try :
1035+ result = _retry_on_server_error (
1036+ lambda : create_dataset (client , org_id , "NDI_PYTEST_DEL_MSG" )
1037+ )
1038+ except _APIError as exc :
1039+ pytest .skip (f"create_dataset timed out: { exc } " )
1040+ ds_id = result .get ("_id" , result .get ("id" , "" ))
1041+ assert ds_id
1042+
1043+ del_result = delete_dataset (client , ds_id , when = "now" )
1044+ assert isinstance (del_result , dict )
1045+ assert "message" in del_result
1046+
1047+ def test_delete_document_returns_message (self , client , fresh_dataset ):
1048+ """delete_document should return a response dict with a message."""
1049+ from ndi .cloud .api .documents import add_document , delete_document
1050+
1051+ doc_json = {
1052+ "document_class" : {"class_name" : "ndi_pytest_delmsg" },
1053+ "base" : {"name" : "delete_msg_test" },
1054+ }
1055+ result = add_document (client , fresh_dataset , doc_json )
1056+ doc_id = result .get ("_id" , result .get ("id" , "" ))
1057+ assert doc_id
1058+
1059+ del_result = delete_document (client , fresh_dataset , doc_id , when = "now" )
1060+ assert isinstance (del_result , dict )
1061+ assert "message" in del_result
1062+
1063+
8761064# ===========================================================================
8771065# TestErrorHandling -- replaces mocked error tests
8781066# ===========================================================================
0 commit comments