@@ -214,54 +214,59 @@ async fn install_simple_example_component() -> Result<(), CliError> {
214214// docs:
215215pub async fn install_blocklist_configmap ( ) -> Result < ( ) , CliError > {
216216 match connect_to_client ( ) . await {
217- Ok ( client) => {
218- println ! (
219- "{} {}" ,
220- "=====>" . blue( ) . bold( ) ,
221- "Checking if the Blocklist configmap exists"
222- ) ;
223- sleep ( Duration :: from_secs ( 1 ) ) ;
224- let blocklist_exists = check_if_blocklist_exists ( client) . await ?;
225- if !blocklist_exists {
217+ Ok ( client) => install_blocklist_configmap_with_client ( client) . await ,
218+ Err ( e) => Err ( CliError :: ClientError ( Error :: Api ( ErrorResponse {
219+ status : "failed" . to_string ( ) ,
220+ message : "Failed to connect to kubernetes client" . to_string ( ) ,
221+ reason : e. to_string ( ) ,
222+ code : 404 ,
223+ } ) ) ) ,
224+ }
225+ }
226+
227+ // docs:
228+ //
229+ // Split out of install_blocklist_configmap so the blocklist-checking logic can be
230+ // exercised in tests against a fake kube Client, without requiring a real cluster.
231+
232+ async fn install_blocklist_configmap_with_client ( client : Client ) -> Result < ( ) , CliError > {
233+ println ! (
234+ "{} {}" ,
235+ "=====>" . blue( ) . bold( ) ,
236+ "Checking if the Blocklist configmap exists"
237+ ) ;
238+ sleep ( Duration :: from_secs ( 1 ) ) ;
239+ let blocklist_exists = check_if_blocklist_exists ( client) . await ?;
240+ if !blocklist_exists {
241+ println ! (
242+ "{} {}" ,
243+ "=====>" . blue( ) . bold( ) ,
244+ "Blocklist configmap does not exist" . red( ) . bold( )
245+ ) ;
246+ sleep ( Duration :: from_secs ( 1 ) ) ;
247+ println ! ( "{} {}" , "=====>" . bold( ) . blue( ) , "Creating configmap" ) ;
248+ let metdata_configs = create_configs ( ) ;
249+ sleep ( Duration :: from_secs ( 1 ) ) ;
250+ match create_config_file ( metdata_configs) . await {
251+ Ok ( _) => {
226252 println ! (
227253 "{} {}" ,
228- "=====>" . blue( ) . bold( ) ,
229- "Blocklist configmap does not exist" . red( ) . bold( )
230- ) ;
231- sleep ( Duration :: from_secs ( 1 ) ) ;
232- println ! ( "{} {}" , "=====>" . bold( ) . blue( ) , "Creating configmap" ) ;
233- let metdata_configs = create_configs ( ) ;
234- sleep ( Duration :: from_secs ( 1 ) ) ;
235- match create_config_file ( metdata_configs) . await {
236- Ok ( _) => {
237- println ! (
238- "{} {}" ,
239- "=====>" . bold( ) . blue( ) ,
240- "Configmap created/repaired successfully" . bold( ) . green( )
241- )
242- }
243- Err ( e) => {
244- return Err ( CliError :: InstallerError {
245- reason : e. to_string ( ) ,
246- } ) ;
247- }
248- }
249- return Ok ( ( ) ) ;
250- } else {
251- println ! ( )
254+ "=====>" . bold( ) . blue( ) ,
255+ "Configmap created/repaired successfully" . bold( ) . green( )
256+ )
257+ }
258+ Err ( e) => {
259+ return Err ( CliError :: InstallerError {
260+ reason : e. to_string ( ) ,
261+ } ) ;
252262 }
253-
254- Ok ( ( ) )
255- }
256- Err ( e) => {
257- return Err ( CliError :: ClientError ( Error :: Api ( ErrorResponse {
258- status : "failed" . to_string ( ) ,
259- message : "Failed to connect to kubernetes client" . to_string ( ) ,
260- reason : e. to_string ( ) ,
261- code : 404 ,
262- } ) ) ) ;
263263 }
264+ return Ok ( ( ) ) ;
265+ } else {
266+ println ! ( )
264267 }
268+
269+ Ok ( ( ) )
265270}
266271
267272// docs:
@@ -298,6 +303,13 @@ async fn check_if_blocklist_exists(client: Client) -> Result<bool, CliError> {
298303//
299304
300305fn install_components ( components_type : & str ) -> Result < ( ) , CliError > {
306+ install_components_with ( & RealCommandRunner , components_type)
307+ }
308+
309+ fn install_components_with (
310+ runner : & dyn CommandRunner ,
311+ components_type : & str ,
312+ ) -> Result < ( ) , CliError > {
301313 if components_type == "cortexbrain" {
302314 let files_to_install = vec ! [
303315 "configmap-role.yaml" ,
@@ -328,7 +340,7 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
328340 "Applying" ,
329341 component. to_string( ) . green( ) . bold( )
330342 ) ;
331- apply_component ( component) ?;
343+ apply_component_with ( runner , component) ?;
332344 i = i + 1 ;
333345 }
334346 } else if components_type == "simple-example" {
@@ -348,7 +360,7 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
348360 "Applying" ,
349361 component. to_string( ) . green( ) . bold( )
350362 ) ;
351- apply_component ( component) ?;
363+ apply_component_with ( runner , component) ?;
352364 i = i + 1 ;
353365 }
354366 } else {
@@ -368,10 +380,10 @@ fn install_components(components_type: &str) -> Result<(), CliError> {
368380//
369381// Returns an CliError if something fails
370382
371- fn apply_component ( file : & str ) -> Result < ( ) , CliError > {
372- let output = Command :: new ( BASE_COMMAND )
373- . args ( [ "apply" , "-f" , file ] )
374- . output ( )
383+ fn apply_component_with ( runner : & dyn CommandRunner , file : & str ) -> Result < ( ) , CliError > {
384+ let args = [ "apply" . to_string ( ) , "-f" . to_string ( ) , file . to_string ( ) ] ;
385+ let output = runner
386+ . run ( BASE_COMMAND , & args )
375387 . map_err ( |e| CliError :: InstallerError {
376388 reason : e. to_string ( ) ,
377389 } ) ?;
@@ -523,14 +535,7 @@ fn rm_file(file_to_remove: &str) -> Result<(), CliError> {
523535#[ cfg( test) ]
524536mod tests {
525537 use super :: * ;
526-
527- #[ test]
528- fn test_rm_file ( ) {
529- let file_name = "test_rm_file.txt" ;
530- std:: fs:: write ( file_name, "test content" ) . unwrap ( ) ;
531- assert ! ( rm_file( file_name) . is_ok( ) ) ;
532- assert ! ( !std:: path:: Path :: new( file_name) . exists( ) ) ;
533- }
538+ use clap:: Parser ;
534539
535540 #[ test]
536541 fn test_download_file_with_success ( ) {
@@ -550,20 +555,6 @@ mod tests {
550555 assert ! ( download_file_with( & runner, "https://example.com/file.yaml" ) . is_err( ) ) ;
551556 }
552557
553- #[ test]
554- fn test_rm_file_failure ( ) {
555- let file_name = "non_existent_file.txt" ;
556- assert ! ( rm_file( file_name) . is_err( ) ) ;
557- }
558-
559- #[ test]
560- fn test_rm_file_cleanup_only ( ) {
561- let file_name = "test_rm_file_cleanup_only.txt" ;
562- std:: fs:: write ( file_name, "test content" ) . unwrap ( ) ;
563- // Clean up without removing
564- rm_file ( file_name) . unwrap ( ) ;
565- }
566-
567558 #[ test]
568559 fn test_rm_file_failure_cleanup ( ) {
569560 let file_name = "test_rm_file_failure_cleanup.txt" ;
@@ -579,18 +570,119 @@ mod tests {
579570 assert ! ( rm_file( file_name) . is_ok( ) ) ;
580571 }
581572
573+ // docs: builds a fake kube Client backed by an in-memory tower service, so the
574+ // Kubernetes API calls never leave the process or touch a real cluster.
575+ fn mock_client ( status : http:: StatusCode , body : String ) -> Client {
576+ let service = tower:: service_fn ( move |_req : http:: Request < kube:: client:: Body > | {
577+ let body = body. clone ( ) ;
578+ async move {
579+ let response = http:: Response :: builder ( )
580+ . status ( status)
581+ . body ( http_body_util:: Full :: new ( bytes:: Bytes :: from ( body) ) )
582+ . unwrap ( ) ;
583+ Ok :: < _ , std:: convert:: Infallible > ( response)
584+ }
585+ } ) ;
586+ Client :: new ( service, "cortexflow" )
587+ }
588+
589+ fn configmap_found_body ( ) -> String {
590+ serde_json:: json!( {
591+ "apiVersion" : "v1" ,
592+ "kind" : "ConfigMap" ,
593+ "metadata" : {
594+ "name" : "cortexbrain-client-config" ,
595+ "namespace" : "cortexflow"
596+ } ,
597+ "data" : { }
598+ } )
599+ . to_string ( )
600+ }
601+
602+ fn configmap_not_found_body ( ) -> String {
603+ serde_json:: json!( {
604+ "apiVersion" : "v1" ,
605+ "kind" : "Status" ,
606+ "status" : "Failure" ,
607+ "message" : "configmaps \" cortexbrain-client-config\" not found" ,
608+ "reason" : "NotFound" ,
609+ "code" : 404
610+ } )
611+ . to_string ( )
612+ }
613+
614+ #[ tokio:: test]
615+ async fn test_check_if_blocklist_exists_true ( ) {
616+ let client = mock_client ( http:: StatusCode :: OK , configmap_found_body ( ) ) ;
617+ let exists = check_if_blocklist_exists ( client) . await . unwrap ( ) ;
618+ assert ! ( exists) ;
619+ }
620+
621+ #[ tokio:: test]
622+ async fn test_check_if_blocklist_exists_false ( ) {
623+ let client = mock_client ( http:: StatusCode :: NOT_FOUND , configmap_not_found_body ( ) ) ;
624+ let exists = check_if_blocklist_exists ( client) . await . unwrap ( ) ;
625+ assert ! ( !exists) ;
626+ }
627+
628+ #[ tokio:: test]
629+ async fn test_install_blocklist_configmap_with_client_already_exists ( ) {
630+ // When the configmap already exists no creation attempt is made, so this
631+ // is safe to exercise end-to-end against the fake client.
632+ let client = mock_client ( http:: StatusCode :: OK , configmap_found_body ( ) ) ;
633+ assert ! ( install_blocklist_configmap_with_client( client) . await . is_ok( ) ) ;
634+ }
635+
582636 #[ test]
583- fn test_rm_file_success_cleanup_only ( ) {
584- let file_name = "test_rm_file_success_cleanup_only.txt" ;
585- std:: fs:: write ( file_name, "test content" ) . unwrap ( ) ;
586- // Clean up a file that exists
587- assert ! ( rm_file( file_name) . is_ok( ) ) ;
637+ fn test_install_components_with_invalid_type ( ) {
638+ let runner = crate :: command_runner:: test_support:: StubCommandRunner :: success ( "" ) ;
639+ assert ! ( install_components_with( & runner, "not-a-real-type" ) . is_err( ) ) ;
588640 }
589641
590642 #[ test]
591- fn test_rm_file_failure_cleanup_only ( ) {
592- let file_name = "non_existent_file.txt" ;
593- // Attempt to clean up a non-existent file
594- assert ! ( rm_file( file_name) . is_err( ) ) ;
643+ fn test_install_components_with_simple_example_success ( ) {
644+ let runner = crate :: command_runner:: test_support:: StubCommandRunner :: success ( "" ) ;
645+ assert ! ( install_components_with( & runner, "simple-example" ) . is_ok( ) ) ;
646+ }
647+
648+ #[ test]
649+ fn test_install_components_with_cortexbrain_success ( ) {
650+ let runner = crate :: command_runner:: test_support:: StubCommandRunner :: success ( "" ) ;
651+ assert ! ( install_components_with( & runner, "cortexbrain" ) . is_ok( ) ) ;
652+ }
653+
654+ #[ test]
655+ fn test_install_components_with_apply_failure ( ) {
656+ let runner = crate :: command_runner:: test_support:: StubCommandRunner :: failure ( "kubectl error" ) ;
657+ assert ! ( install_components_with( & runner, "simple-example" ) . is_err( ) ) ;
658+ }
659+
660+ #[ derive( clap:: Parser ) ]
661+ struct InstallCommandsHarness {
662+ #[ command( subcommand) ]
663+ cmd : InstallCommands ,
664+ }
665+
666+ #[ test]
667+ fn test_install_commands_parses_cortexflow ( ) {
668+ let parsed = InstallCommandsHarness :: try_parse_from ( [ "cfcli" , "cortexflow" ] ) . unwrap ( ) ;
669+ assert ! ( matches!( parsed. cmd, InstallCommands :: All ) ) ;
670+ }
671+
672+ #[ test]
673+ fn test_install_commands_parses_simple_example ( ) {
674+ let parsed = InstallCommandsHarness :: try_parse_from ( [ "cfcli" , "simple-example" ] ) . unwrap ( ) ;
675+ assert ! ( matches!( parsed. cmd, InstallCommands :: TestPods ) ) ;
676+ }
677+
678+ #[ test]
679+ fn test_install_commands_parses_blocklist ( ) {
680+ let parsed = InstallCommandsHarness :: try_parse_from ( [ "cfcli" , "blocklist" ] ) . unwrap ( ) ;
681+ assert ! ( matches!( parsed. cmd, InstallCommands :: Blocklist ) ) ;
682+ }
683+
684+ #[ test]
685+ fn test_install_commands_rejects_unknown_subcommand ( ) {
686+ assert ! ( InstallCommandsHarness :: try_parse_from( [ "cfcli" , "not-a-command" ] ) . is_err( ) ) ;
595687 }
596688}
0 commit comments