Skip to content

Commit 29afeb6

Browse files
ncodeclaude
andcommitted
test: add more coverage tests for edge cases
- TestRun_SmallTTL: covers TTL < 2s branch in Run() - TestRun_ElectionErrorInLoop: covers error handling in ticker loop - TestUpdateLeadershipStatus_Error: covers error return path - TestNew_ViperUnmarshalError: covers viper unmarshal failure - TestNew_DefaultValues: covers default LockDelay/TTL/Name paths 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c04524c commit 29afeb6

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

internal/ballot/ballot_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)