1010from unittest .mock import MagicMock , patch
1111
1212from hysds_commons .search_utils import SearchUtility
13+ from hysds_commons .opensearch_utils import OpenSearchUtility
1314
1415
1516class ConcreteSearchUtility (SearchUtility ):
@@ -23,6 +24,15 @@ def __init__(self):
2324 self .flavor = "default"
2425
2526
27+ class MockOpenSearchUtility (OpenSearchUtility ):
28+ """Mock OpenSearchUtility that skips real OpenSearch client initialization."""
29+ def __init__ (self ):
30+ self .es = MagicMock ()
31+ self .engine = "opensearch"
32+ self .version = None
33+ self .flavor = None
34+
35+
2636class TestIsWildcardIndex :
2737 """Tests for _is_wildcard_index() static method."""
2838
@@ -294,27 +304,125 @@ def setup_method(self):
294304 }
295305 self .utility .es .close_point_in_time .return_value = {"succeeded" : True }
296306
307+ def test_pit_does_not_pass_allow_no_indices_to_open_pit (self ):
308+ """open_point_in_time() should NOT receive allow_no_indices."""
309+ self .utility ._pit (index = "job_status-*" , body = {"query" : {"match_all" : {}}})
310+ call_kwargs = self .utility .es .open_point_in_time .call_args [1 ]
311+ assert "allow_no_indices" not in call_kwargs
312+ assert call_kwargs ["ignore_unavailable" ] is True
313+ assert call_kwargs ["expand_wildcards" ] == "open"
314+
315+ def test_pit_search_does_not_pass_indices_options (self ):
316+ """_search calls with PIT body must NOT include indicesOptions."""
317+ self .utility ._pit (index = "job_status-*" , body = {"query" : {"match_all" : {}}})
318+ call_kwargs = self .utility .es .search .call_args [1 ]
319+ assert "ignore_unavailable" not in call_kwargs
320+ assert "allow_no_indices" not in call_kwargs
321+ assert "expand_wildcards" not in call_kwargs
322+
297323 def test_pit_with_wildcard_applies_params_to_open_point_in_time (self ):
298324 """_pit() should apply closed index params to open_point_in_time for wildcard patterns."""
299325 self .utility ._pit (index = "job_status-*" , body = {"query" : {"match_all" : {}}})
300326
301- # Verify open_point_in_time was called with closed index params
327+ # Verify open_point_in_time was called with closed index params (except allow_no_indices)
302328 call_kwargs = self .utility .es .open_point_in_time .call_args [1 ]
303329 assert call_kwargs ["ignore_unavailable" ] is True
304- assert call_kwargs ["allow_no_indices" ] is True
305330 assert call_kwargs ["expand_wildcards" ] == "open"
306331
307332 def test_pit_with_single_index_applies_params_to_open_point_in_time (self ):
308333 """_pit() should apply params to open_point_in_time for single index (could be alias)."""
309334 self .utility ._pit (index = "job_status-current" , body = {"query" : {"match_all" : {}}})
310335
311- # Verify open_point_in_time was called with closed index params
336+ # Verify open_point_in_time was called with closed index params (except allow_no_indices)
312337 call_kwargs = self .utility .es .open_point_in_time .call_args [1 ]
313338 assert call_kwargs ["ignore_unavailable" ] is True
314- assert call_kwargs ["allow_no_indices" ] is True
315339 assert call_kwargs ["expand_wildcards" ] == "open"
316340
317341
342+ class TestOpenSearchPitMethod :
343+ """Tests for OpenSearchUtility._pit() with closed index handling (HC-600)."""
344+
345+ def setup_method (self ):
346+ """Set up test fixtures."""
347+ self .utility = MockOpenSearchUtility ()
348+ self .utility .es .create_point_in_time .return_value = {
349+ "pit_id" : "opensearch_pit_id_123"
350+ }
351+ self .utility .es .search .return_value = {
352+ "hits" : {"total" : {"value" : 0 }, "hits" : []}
353+ }
354+ self .utility .es .delete_point_in_time .return_value = {"succeeded" : True }
355+
356+ def test_create_pit_applies_closed_index_params (self ):
357+ """create_point_in_time() should receive ignore_unavailable and expand_wildcards."""
358+ self .utility ._pit (index = "grq_v1.0_product-*" , body = {"query" : {"match_all" : {}}})
359+ call_kwargs = self .utility .es .create_point_in_time .call_args [1 ]
360+ assert call_kwargs ["ignore_unavailable" ] is True
361+ assert call_kwargs ["expand_wildcards" ] == "open"
362+
363+ def test_create_pit_does_not_pass_allow_no_indices (self ):
364+ """PIT open APIs don't accept allow_no_indices -- must not be passed."""
365+ self .utility ._pit (index = "grq" , body = {"query" : {"match_all" : {}}})
366+ call_kwargs = self .utility .es .create_point_in_time .call_args [1 ]
367+ assert "allow_no_indices" not in call_kwargs
368+
369+ def test_create_pit_applies_params_for_alias (self ):
370+ """create_point_in_time() should apply params for aliases (no wildcard in name)."""
371+ self .utility ._pit (index = "grq" , body = {"query" : {"match_all" : {}}})
372+ call_kwargs = self .utility .es .create_point_in_time .call_args [1 ]
373+ assert call_kwargs ["ignore_unavailable" ] is True
374+ assert call_kwargs ["expand_wildcards" ] == "open"
375+
376+ def test_create_pit_does_not_override_caller_params (self ):
377+ """Caller-specified closed-index params should not be overridden."""
378+ self .utility ._pit (
379+ index = "grq" ,
380+ body = {"query" : {"match_all" : {}}},
381+ ignore_unavailable = False ,
382+ expand_wildcards = "all" ,
383+ )
384+ call_kwargs = self .utility .es .create_point_in_time .call_args [1 ]
385+ assert call_kwargs ["ignore_unavailable" ] is False
386+ assert call_kwargs ["expand_wildcards" ] == "all"
387+
388+ def test_pit_search_does_not_pass_indices_options (self ):
389+ """_search calls with PIT must NOT include indicesOptions."""
390+ self .utility ._pit (index = "grq" , body = {"query" : {"match_all" : {}}})
391+ call_kwargs = self .utility .es .search .call_args [1 ]
392+ assert "ignore_unavailable" not in call_kwargs
393+ assert "allow_no_indices" not in call_kwargs
394+ assert "expand_wildcards" not in call_kwargs
395+
396+ def test_pit_pagination_still_works (self ):
397+ """Verify pagination loop + PIT cleanup still functions correctly."""
398+ page1 = {
399+ "hits" : {
400+ "total" : {"value" : 2 },
401+ "hits" : [
402+ {"_id" : "1" , "_source" : {"id" : "a" }, "sort" : [1 , "a" ]},
403+ {"_id" : "2" , "_source" : {"id" : "b" }, "sort" : [2 , "b" ]},
404+ ],
405+ }
406+ }
407+ page2 = {"hits" : {"total" : {"value" : 2 }, "hits" : []}}
408+ self .utility .es .search .side_effect = [page1 , page2 ]
409+
410+ records = self .utility ._pit (index = "grq" , body = {"query" : {"match_all" : {}}})
411+
412+ assert len (records ) == 2
413+ assert records [0 ]["_id" ] == "1"
414+ assert records [1 ]["_id" ] == "2"
415+ # Verify PIT was cleaned up
416+ self .utility .es .delete_point_in_time .assert_called_once_with (
417+ body = {"pit_id" : ["opensearch_pit_id_123" ]}
418+ )
419+
420+ def test_missing_index_raises_runtime_error (self ):
421+ """_pit() should raise RuntimeError when no index is provided."""
422+ with pytest .raises (RuntimeError , match = "must specify a index/alias" ):
423+ self .utility ._pit (body = {"query" : {"match_all" : {}}})
424+
425+
318426class TestClosedIndexParamsConstant :
319427 """Tests for CLOSED_INDEX_PARAMS class constant."""
320428
0 commit comments