Skip to content

Commit c04524c

Browse files
ncodeclaude
andcommitted
test: improve coverage from 84.9% to 90.5%
Add tests for previously uncovered code paths: - consulClient wrapper methods (Agent, Catalog, Health, Session, KV) - commandExecutor.CommandContext - updateServiceTags async command execution (ExecOnPromote/ExecOnDemote) - Error handling in command execution goroutine 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent cadac98 commit c04524c

File tree

1 file changed

+372
-0
lines changed

1 file changed

+372
-0
lines changed

internal/ballot/ballot_test.go

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

Comments
 (0)