@@ -2728,3 +2728,188 @@ func TestUpdateServiceTags_WithCommandExecution(t *testing.T) {
27282728 time .Sleep (200 * time .Millisecond )
27292729 })
27302730}
2731+
2732+ func TestRun_SmallTTL (t * testing.T ) {
2733+ t .Run ("Run with TTL less than 2 seconds uses 1 second interval" , func (t * testing.T ) {
2734+ ctx , cancel := context .WithCancel (context .Background ())
2735+
2736+ sessionID := "session_id"
2737+ b := & Ballot {
2738+ ID : "test_service_id" ,
2739+ Name : "test_service" ,
2740+ Key : "election/test_service/leader" ,
2741+ PrimaryTag : "primary" ,
2742+ TTL : 500 * time .Millisecond , // TTL/2 = 250ms < 1s, so interval becomes 1s
2743+ ctx : ctx ,
2744+ }
2745+ b .sessionID .Store (& sessionID )
2746+
2747+ mockHealth := new (MockHealth )
2748+ mockHealth .On ("Checks" , b .Name , mock .Anything ).Return ([]* api.HealthCheck {
2749+ {Status : "passing" },
2750+ }, nil , nil )
2751+
2752+ mockSession := new (MockSession )
2753+ mockSession .On ("Create" , mock .Anything , mock .Anything ).Return (sessionID , nil , nil )
2754+ mockSession .On ("RenewPeriodic" , mock .Anything , sessionID , mock .Anything , mock .Anything ).Return (nil )
2755+ mockSession .On ("Info" , sessionID , mock .Anything ).Return (& api.SessionEntry {ID : sessionID }, & api.QueryMeta {}, nil )
2756+
2757+ payload := & ElectionPayload {
2758+ Address : "127.0.0.1" ,
2759+ Port : 8080 ,
2760+ SessionID : sessionID ,
2761+ }
2762+ data , _ := json .Marshal (payload )
2763+ mockKV := new (MockKV )
2764+ mockKV .On ("Acquire" , mock .Anything , mock .Anything ).Return (true , nil , nil )
2765+ mockKV .On ("Get" , b .Key , mock .Anything ).Return (& api.KVPair {
2766+ Key : b .Key ,
2767+ Value : data ,
2768+ Session : sessionID ,
2769+ }, nil , nil )
2770+
2771+ service := & api.AgentService {
2772+ ID : b .ID ,
2773+ Service : b .Name ,
2774+ Address : "127.0.0.1" ,
2775+ Port : 8080 ,
2776+ Tags : []string {},
2777+ }
2778+ mockAgent := new (MockAgent )
2779+ mockAgent .On ("Service" , b .ID , mock .Anything ).Return (service , nil , nil )
2780+ mockAgent .On ("ServiceRegister" , mock .Anything ).Return (nil )
2781+
2782+ mockCatalog := new (MockCatalog )
2783+ mockCatalog .On ("Service" , b .Name , b .PrimaryTag , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2784+ mockCatalog .On ("Service" , b .Name , "" , mock .Anything ).Return ([]* api.CatalogService {}, nil , nil )
2785+ mockCatalog .On ("Register" , mock .Anything , mock .Anything ).Return (nil , nil )
2786+
2787+ mockClient := & MockConsulClient {}
2788+ mockClient .On ("Health" ).Return (mockHealth )
2789+ mockClient .On ("Session" ).Return (mockSession )
2790+ mockClient .On ("KV" ).Return (mockKV )
2791+ mockClient .On ("Agent" ).Return (mockAgent )
2792+ mockClient .On ("Catalog" ).Return (mockCatalog )
2793+
2794+ b .client = mockClient
2795+
2796+ done := make (chan error , 1 )
2797+ go func () {
2798+ done <- b .Run ()
2799+ }()
2800+
2801+ // Let it run briefly then cancel
2802+ time .Sleep (100 * time .Millisecond )
2803+ cancel ()
2804+
2805+ err := <- done
2806+ assert .NoError (t , err )
2807+ })
2808+ }
2809+
2810+ func TestRun_ElectionErrorInLoop (t * testing.T ) {
2811+ t .Run ("Run handles election errors in ticker loop" , func (t * testing.T ) {
2812+ ctx , cancel := context .WithCancel (context .Background ())
2813+
2814+ sessionID := "session_id"
2815+ b := & Ballot {
2816+ ID : "test_service_id" ,
2817+ Name : "test_service" ,
2818+ Key : "election/test_service/leader" ,
2819+ PrimaryTag : "primary" ,
2820+ TTL : 100 * time .Millisecond ,
2821+ ctx : ctx ,
2822+ }
2823+ b .sessionID .Store (& sessionID )
2824+
2825+ // Mock health to return error (triggers election error)
2826+ mockHealth := new (MockHealth )
2827+ electionErr := errors .New ("health check failed" )
2828+ mockHealth .On ("Checks" , b .Name , mock .Anything ).Return (nil , nil , electionErr )
2829+
2830+ mockClient := & MockConsulClient {}
2831+ mockClient .On ("Health" ).Return (mockHealth )
2832+
2833+ b .client = mockClient
2834+
2835+ done := make (chan error , 1 )
2836+ go func () {
2837+ done <- b .Run ()
2838+ }()
2839+
2840+ // Let it run through at least one ticker cycle with error
2841+ time .Sleep (200 * time .Millisecond )
2842+ cancel ()
2843+
2844+ err := <- done
2845+ assert .NoError (t , err ) // Run returns nil on context cancellation, errors are logged
2846+ })
2847+ }
2848+
2849+ func TestUpdateLeadershipStatus_Error (t * testing.T ) {
2850+ t .Run ("updateLeadershipStatus returns error from updateServiceTags" , func (t * testing.T ) {
2851+ mockAgent := new (MockAgent )
2852+ expectedErr := errors .New ("service tags update failed" )
2853+ mockAgent .On ("Service" , "test_id" , mock .Anything ).Return (nil , nil , expectedErr )
2854+
2855+ mockClient := & MockConsulClient {}
2856+ mockClient .On ("Agent" ).Return (mockAgent )
2857+
2858+ b := & Ballot {
2859+ ID : "test_id" ,
2860+ client : mockClient ,
2861+ }
2862+
2863+ err := b .updateLeadershipStatus (true )
2864+ assert .Error (t , err )
2865+ assert .Equal (t , expectedErr , err )
2866+ })
2867+ }
2868+
2869+ func TestNew_ViperUnmarshalError (t * testing.T ) {
2870+ t .Run ("New returns error when viper unmarshal fails" , func (t * testing.T ) {
2871+ viper .Reset ()
2872+ // Set an invalid type that will cause unmarshal to fail
2873+ // TTL expects a duration string but we give it an invalid map
2874+ viper .Set ("election.services.badconfig.ttl" , map [string ]string {"invalid" : "type" })
2875+ viper .Set ("election.services.badconfig.id" , "test_id" )
2876+ viper .Set ("election.services.badconfig.key" , "test/key" )
2877+
2878+ defer viper .Reset ()
2879+
2880+ b , err := New (context .Background (), "badconfig" )
2881+ assert .Error (t , err )
2882+ assert .Nil (t , b )
2883+ })
2884+ }
2885+
2886+ func TestNew_DefaultValues (t * testing.T ) {
2887+ t .Run ("New sets default LockDelay and TTL when not specified" , func (t * testing.T ) {
2888+ viper .Reset ()
2889+ viper .Set ("election.services.defaults.id" , "test_service_id" )
2890+ viper .Set ("election.services.defaults.key" , "election/test/leader" )
2891+ // Don't set TTL or LockDelay
2892+
2893+ defer viper .Reset ()
2894+
2895+ b , err := New (context .Background (), "defaults" )
2896+ assert .NoError (t , err )
2897+ assert .NotNil (t , b )
2898+ assert .Equal (t , 3 * time .Second , b .LockDelay )
2899+ assert .Equal (t , 10 * time .Second , b .TTL )
2900+ })
2901+
2902+ t .Run ("New sets Name from parameter when not in config" , func (t * testing.T ) {
2903+ viper .Reset ()
2904+ viper .Set ("election.services.myservice.id" , "test_service_id" )
2905+ viper .Set ("election.services.myservice.key" , "election/test/leader" )
2906+ // Don't set Name
2907+
2908+ defer viper .Reset ()
2909+
2910+ b , err := New (context .Background (), "myservice" )
2911+ assert .NoError (t , err )
2912+ assert .NotNil (t , b )
2913+ assert .Equal (t , "myservice" , b .Name )
2914+ })
2915+ }
0 commit comments