@@ -2356,3 +2356,375 @@ func TestHandleServiceCriticalState_WarningState(t *testing.T) {
23562356 err := b .handleServiceCriticalState ()
23572357 assert .NoError (t , err )
23582358}
2359+
2360+ func TestConsulClientWrappers (t * testing.T ) {
2361+ // Test consulClient wrapper methods
2362+ // These tests verify that the wrapper methods correctly delegate to the underlying api.Client
2363+ // Note: These tests use a nil api.Client because we're testing the wrapper structure,
2364+ // not the actual Consul API calls
2365+
2366+ t .Run ("consulClient Agent returns AgentWrapper" , func (t * testing.T ) {
2367+ // Create a consul client with default config (will work even without running Consul)
2368+ apiClient , err := api .NewClient (api .DefaultConfig ())
2369+ assert .NoError (t , err )
2370+
2371+ cc := & consulClient {client : apiClient }
2372+ agent := cc .Agent ()
2373+ assert .NotNil (t , agent )
2374+
2375+ // Verify it returns an AgentWrapper
2376+ _ , ok := agent .(* AgentWrapper )
2377+ assert .True (t , ok , "Agent() should return *AgentWrapper" )
2378+ })
2379+
2380+ t .Run ("consulClient Catalog returns CatalogWrapper" , func (t * testing.T ) {
2381+ apiClient , err := api .NewClient (api .DefaultConfig ())
2382+ assert .NoError (t , err )
2383+
2384+ cc := & consulClient {client : apiClient }
2385+ catalog := cc .Catalog ()
2386+ assert .NotNil (t , catalog )
2387+
2388+ _ , ok := catalog .(* CatalogWrapper )
2389+ assert .True (t , ok , "Catalog() should return *CatalogWrapper" )
2390+ })
2391+
2392+ t .Run ("consulClient Health returns HealthWrapper" , func (t * testing.T ) {
2393+ apiClient , err := api .NewClient (api .DefaultConfig ())
2394+ assert .NoError (t , err )
2395+
2396+ cc := & consulClient {client : apiClient }
2397+ health := cc .Health ()
2398+ assert .NotNil (t , health )
2399+
2400+ _ , ok := health .(* HealthWrapper )
2401+ assert .True (t , ok , "Health() should return *HealthWrapper" )
2402+ })
2403+
2404+ t .Run ("consulClient Session returns SessionWrapper" , func (t * testing.T ) {
2405+ apiClient , err := api .NewClient (api .DefaultConfig ())
2406+ assert .NoError (t , err )
2407+
2408+ cc := & consulClient {client : apiClient }
2409+ session := cc .Session ()
2410+ assert .NotNil (t , session )
2411+
2412+ _ , ok := session .(* SessionWrapper )
2413+ assert .True (t , ok , "Session() should return *SessionWrapper" )
2414+ })
2415+
2416+ t .Run ("consulClient KV returns KVWrapper" , func (t * testing.T ) {
2417+ apiClient , err := api .NewClient (api .DefaultConfig ())
2418+ assert .NoError (t , err )
2419+
2420+ cc := & consulClient {client : apiClient }
2421+ kv := cc .KV ()
2422+ assert .NotNil (t , kv )
2423+
2424+ _ , ok := kv .(* KVWrapper )
2425+ assert .True (t , ok , "KV() should return *KVWrapper" )
2426+ })
2427+ }
2428+
2429+ func TestCommandExecutor (t * testing.T ) {
2430+ t .Run ("CommandContext creates exec.Cmd" , func (t * testing.T ) {
2431+ executor := & commandExecutor {}
2432+ ctx := context .Background ()
2433+
2434+ cmd := executor .CommandContext (ctx , "echo" , "hello" )
2435+ assert .NotNil (t , cmd )
2436+ // Path is resolved to full path by exec.LookPath, check it contains "echo"
2437+ assert .Contains (t , cmd .Path , "echo" )
2438+ assert .Contains (t , cmd .Args , "echo" )
2439+ assert .Contains (t , cmd .Args , "hello" )
2440+ })
2441+
2442+ t .Run ("CommandContext with no args" , func (t * testing.T ) {
2443+ executor := & commandExecutor {}
2444+ ctx := context .Background ()
2445+
2446+ cmd := executor .CommandContext (ctx , "true" )
2447+ assert .NotNil (t , cmd )
2448+ })
2449+ }
2450+
2451+ func TestHealthWrapper_Checks (t * testing.T ) {
2452+ // Test HealthWrapper directly with a mock Health
2453+ apiClient , err := api .NewClient (api .DefaultConfig ())
2454+ assert .NoError (t , err )
2455+
2456+ hw := & HealthWrapper {health : apiClient .Health ()}
2457+ assert .NotNil (t , hw )
2458+
2459+ // We can't actually call Checks without a running Consul,
2460+ // but we verify the wrapper is correctly structured
2461+ }
2462+
2463+ func TestUpdateServiceTags_WithCommandExecution (t * testing.T ) {
2464+ t .Run ("Executes ExecOnPromote when becoming leader" , func (t * testing.T ) {
2465+ serviceID := "test_service_id"
2466+ serviceName := "test_service"
2467+ primaryTag := "primary"
2468+ sessionID := "session_id"
2469+
2470+ mockAgent := new (MockAgent )
2471+ mockCatalog := new (MockCatalog )
2472+ mockKV := new (MockKV )
2473+ mockClient := & MockConsulClient {}
2474+
2475+ // Service without primary tag
2476+ baseService := & api.AgentService {
2477+ ID : serviceID ,
2478+ Service : serviceName ,
2479+ Tags : []string {"tag1" },
2480+ Port : 8080 ,
2481+ Address : "127.0.0.1" ,
2482+ }
2483+
2484+ mockAgent .On ("Service" , serviceID , mock .Anything ).Return (baseService , nil , nil )
2485+ mockCatalog .On ("Service" , serviceName , primaryTag , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2486+ mockAgent .On ("ServiceRegister" , mock .Anything ).Return (nil )
2487+
2488+ // Mock KV for session data retrieval
2489+ payload := & ElectionPayload {
2490+ Address : "127.0.0.1" ,
2491+ Port : 8080 ,
2492+ SessionID : sessionID ,
2493+ }
2494+ data , _ := json .Marshal (payload )
2495+ mockKV .On ("Get" , "election/test/leader" , mock .Anything ).Return (& api.KVPair {
2496+ Key : "election/test/leader" ,
2497+ Value : data ,
2498+ }, nil , nil )
2499+
2500+ mockClient .On ("Agent" ).Return (mockAgent )
2501+ mockClient .On ("Catalog" ).Return (mockCatalog )
2502+ mockClient .On ("KV" ).Return (mockKV )
2503+
2504+ // Use a mock executor that tracks calls
2505+ mockExecutor := new (MockCommandExecutor )
2506+ mockCmd := exec .Command ("echo" , "promoted" )
2507+ mockExecutor .On ("CommandContext" , mock .Anything , "echo" , []string {"promoted" }).Return (mockCmd )
2508+
2509+ ctx , cancel := context .WithCancel (context .Background ())
2510+ defer cancel ()
2511+
2512+ b := & Ballot {
2513+ client : mockClient ,
2514+ ID : serviceID ,
2515+ Name : serviceName ,
2516+ PrimaryTag : primaryTag ,
2517+ Key : "election/test/leader" ,
2518+ ctx : ctx ,
2519+ ExecOnPromote : "echo promoted" ,
2520+ executor : mockExecutor ,
2521+ TTL : 10 * time .Second ,
2522+ LockDelay : 3 * time .Second ,
2523+ }
2524+ b .sessionID .Store (& sessionID )
2525+
2526+ err := b .updateServiceTags (true )
2527+ assert .NoError (t , err )
2528+
2529+ // Give goroutine time to execute
2530+ time .Sleep (1500 * time .Millisecond )
2531+
2532+ mockAgent .AssertCalled (t , "ServiceRegister" , mock .Anything )
2533+ })
2534+
2535+ t .Run ("Executes ExecOnDemote when losing leadership" , func (t * testing.T ) {
2536+ serviceID := "test_service_id"
2537+ serviceName := "test_service"
2538+ primaryTag := "primary"
2539+ sessionID := "session_id"
2540+
2541+ mockAgent := new (MockAgent )
2542+ mockCatalog := new (MockCatalog )
2543+ mockKV := new (MockKV )
2544+ mockClient := & MockConsulClient {}
2545+
2546+ // Service with primary tag
2547+ serviceWithTag := & api.AgentService {
2548+ ID : serviceID ,
2549+ Service : serviceName ,
2550+ Tags : []string {"tag1" , primaryTag },
2551+ Port : 8080 ,
2552+ Address : "127.0.0.1" ,
2553+ }
2554+
2555+ mockAgent .On ("Service" , serviceID , mock .Anything ).Return (serviceWithTag , nil , nil )
2556+ mockCatalog .On ("Service" , serviceName , primaryTag , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2557+ mockAgent .On ("ServiceRegister" , mock .Anything ).Return (nil )
2558+
2559+ // Mock KV for session data retrieval
2560+ payload := & ElectionPayload {
2561+ Address : "127.0.0.1" ,
2562+ Port : 8080 ,
2563+ SessionID : sessionID ,
2564+ }
2565+ data , _ := json .Marshal (payload )
2566+ mockKV .On ("Get" , "election/test/leader" , mock .Anything ).Return (& api.KVPair {
2567+ Key : "election/test/leader" ,
2568+ Value : data ,
2569+ }, nil , nil )
2570+
2571+ mockClient .On ("Agent" ).Return (mockAgent )
2572+ mockClient .On ("Catalog" ).Return (mockCatalog )
2573+ mockClient .On ("KV" ).Return (mockKV )
2574+
2575+ // Use a mock executor
2576+ mockExecutor := new (MockCommandExecutor )
2577+ mockCmd := exec .Command ("echo" , "demoted" )
2578+ mockExecutor .On ("CommandContext" , mock .Anything , "echo" , []string {"demoted" }).Return (mockCmd )
2579+
2580+ ctx , cancel := context .WithCancel (context .Background ())
2581+ defer cancel ()
2582+
2583+ b := & Ballot {
2584+ client : mockClient ,
2585+ ID : serviceID ,
2586+ Name : serviceName ,
2587+ PrimaryTag : primaryTag ,
2588+ Key : "election/test/leader" ,
2589+ ctx : ctx ,
2590+ ExecOnDemote : "echo demoted" ,
2591+ executor : mockExecutor ,
2592+ TTL : 10 * time .Second ,
2593+ LockDelay : 3 * time .Second ,
2594+ }
2595+ b .sessionID .Store (& sessionID )
2596+
2597+ err := b .updateServiceTags (false )
2598+ assert .NoError (t , err )
2599+
2600+ // Give goroutine time to execute
2601+ time .Sleep (1500 * time .Millisecond )
2602+
2603+ mockAgent .AssertCalled (t , "ServiceRegister" , mock .Anything )
2604+ })
2605+
2606+ t .Run ("Handles command execution error gracefully" , func (t * testing.T ) {
2607+ serviceID := "test_service_id"
2608+ serviceName := "test_service"
2609+ primaryTag := "primary"
2610+ sessionID := "session_id"
2611+
2612+ mockAgent := new (MockAgent )
2613+ mockCatalog := new (MockCatalog )
2614+ mockKV := new (MockKV )
2615+ mockClient := & MockConsulClient {}
2616+
2617+ baseService := & api.AgentService {
2618+ ID : serviceID ,
2619+ Service : serviceName ,
2620+ Tags : []string {"tag1" },
2621+ Port : 8080 ,
2622+ Address : "127.0.0.1" ,
2623+ }
2624+
2625+ mockAgent .On ("Service" , serviceID , mock .Anything ).Return (baseService , nil , nil )
2626+ mockCatalog .On ("Service" , serviceName , primaryTag , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2627+ mockAgent .On ("ServiceRegister" , mock .Anything ).Return (nil )
2628+
2629+ // Mock KV for session data retrieval
2630+ payload := & ElectionPayload {
2631+ Address : "127.0.0.1" ,
2632+ Port : 8080 ,
2633+ SessionID : sessionID ,
2634+ }
2635+ data , _ := json .Marshal (payload )
2636+ mockKV .On ("Get" , "election/test/leader" , mock .Anything ).Return (& api.KVPair {
2637+ Key : "election/test/leader" ,
2638+ Value : data ,
2639+ }, nil , nil )
2640+
2641+ mockClient .On ("Agent" ).Return (mockAgent )
2642+ mockClient .On ("Catalog" ).Return (mockCatalog )
2643+ mockClient .On ("KV" ).Return (mockKV )
2644+
2645+ // Use a mock executor that returns a failing command
2646+ mockExecutor := new (MockCommandExecutor )
2647+ mockCmd := exec .Command ("false" ) // 'false' command exits with code 1
2648+ mockExecutor .On ("CommandContext" , mock .Anything , "false" , []string {}).Return (mockCmd )
2649+
2650+ ctx , cancel := context .WithCancel (context .Background ())
2651+ defer cancel ()
2652+
2653+ b := & Ballot {
2654+ client : mockClient ,
2655+ ID : serviceID ,
2656+ Name : serviceName ,
2657+ PrimaryTag : primaryTag ,
2658+ Key : "election/test/leader" ,
2659+ ctx : ctx ,
2660+ ExecOnPromote : "false" ,
2661+ executor : mockExecutor ,
2662+ TTL : 10 * time .Second ,
2663+ LockDelay : 3 * time .Second ,
2664+ }
2665+ b .sessionID .Store (& sessionID )
2666+
2667+ // Should not return error even if command fails
2668+ err := b .updateServiceTags (true )
2669+ assert .NoError (t , err )
2670+
2671+ // Give goroutine time to execute
2672+ time .Sleep (1500 * time .Millisecond )
2673+ })
2674+
2675+ t .Run ("Handles session data retrieval error in command goroutine" , func (t * testing.T ) {
2676+ serviceID := "test_service_id"
2677+ serviceName := "test_service"
2678+ primaryTag := "primary"
2679+ sessionID := "session_id"
2680+
2681+ mockAgent := new (MockAgent )
2682+ mockCatalog := new (MockCatalog )
2683+ mockKV := new (MockKV )
2684+ mockClient := & MockConsulClient {}
2685+
2686+ baseService := & api.AgentService {
2687+ ID : serviceID ,
2688+ Service : serviceName ,
2689+ Tags : []string {"tag1" },
2690+ Port : 8080 ,
2691+ Address : "127.0.0.1" ,
2692+ }
2693+
2694+ mockAgent .On ("Service" , serviceID , mock .Anything ).Return (baseService , nil , nil )
2695+ mockCatalog .On ("Service" , serviceName , primaryTag , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2696+ mockAgent .On ("ServiceRegister" , mock .Anything ).Return (nil )
2697+
2698+ // Mock KV to return error
2699+ mockKV .On ("Get" , "election/test/leader" , mock .Anything ).Return (nil , nil , errors .New ("kv error" ))
2700+
2701+ mockClient .On ("Agent" ).Return (mockAgent )
2702+ mockClient .On ("Catalog" ).Return (mockCatalog )
2703+ mockClient .On ("KV" ).Return (mockKV )
2704+
2705+ mockExecutor := new (MockCommandExecutor )
2706+
2707+ ctx , cancel := context .WithTimeout (context .Background (), 100 * time .Millisecond )
2708+ defer cancel ()
2709+
2710+ b := & Ballot {
2711+ client : mockClient ,
2712+ ID : serviceID ,
2713+ Name : serviceName ,
2714+ PrimaryTag : primaryTag ,
2715+ Key : "election/test/leader" ,
2716+ ctx : ctx ,
2717+ ExecOnPromote : "echo test" ,
2718+ executor : mockExecutor ,
2719+ TTL : 10 * time .Millisecond ,
2720+ LockDelay : 3 * time .Millisecond ,
2721+ }
2722+ b .sessionID .Store (& sessionID )
2723+
2724+ err := b .updateServiceTags (true )
2725+ assert .NoError (t , err )
2726+
2727+ // Wait for goroutine to handle the error
2728+ time .Sleep (200 * time .Millisecond )
2729+ })
2730+ }
0 commit comments