Skip to content

Commit 4fb04a0

Browse files
committed
(feat) start moving things to a factory to simplify the test
1 parent 7a33e19 commit 4fb04a0

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed

pkg/consul/factory.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright © 2025 Juliano Martinez <[email protected]>
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package consul
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/hashicorp/consul/api"
22+
)
23+
24+
// ConsulClient is an interface for the Consul client.
25+
type ConsulClient interface {
26+
Agent() ConsulAgent
27+
}
28+
29+
// ConsulAgent is an interface for the Consul agent.
30+
type ConsulAgent interface {
31+
Service(string, *api.QueryOptions) (*api.AgentService, *api.QueryMeta, error)
32+
ServiceRegister(*api.AgentServiceRegistration) error
33+
}
34+
35+
// ConsulAPIWrapper wraps the Consul API client to conform to the ConsulClient interface.
36+
type ConsulAPIWrapper struct {
37+
client *api.Client
38+
}
39+
40+
// NewConsulAPIWrapper creates a new instance of ConsulAPIWrapper.
41+
func NewConsulAPIWrapper(client *api.Client) *ConsulAPIWrapper {
42+
return &ConsulAPIWrapper{client: client}
43+
}
44+
45+
// Agent returns an object that conforms to the ConsulAgent interface.
46+
func (w *ConsulAPIWrapper) Agent() ConsulAgent {
47+
return w.client.Agent()
48+
}
49+
50+
// ClientFactory is an interface for creating Consul clients
51+
type ClientFactory interface {
52+
NewClient(address, token string) (ConsulClient, error)
53+
}
54+
55+
// DefaultFactory creates real Consul clients
56+
type DefaultFactory struct{}
57+
58+
// NewClient creates a new Consul client with the given configuration
59+
func (f *DefaultFactory) NewClient(address, token string) (ConsulClient, error) {
60+
config := api.DefaultConfig()
61+
config.Address = address
62+
config.Token = token
63+
64+
client, err := api.NewClient(config)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to create Consul client: %w", err)
67+
}
68+
69+
return NewConsulAPIWrapper(client), nil
70+
}
71+
72+
// MockFactory creates mock Consul clients for testing
73+
type MockFactory struct {
74+
MockClient ConsulClient
75+
MockError error
76+
}
77+
78+
// NewClient returns the mock client or error
79+
func (f *MockFactory) NewClient(address, token string) (ConsulClient, error) {
80+
if f.MockError != nil {
81+
return nil, f.MockError
82+
}
83+
return f.MockClient, nil
84+
}
85+
86+
// Factory is the global factory instance (can be overridden for testing)
87+
var Factory ClientFactory = &DefaultFactory{}
88+
89+
// SetFactory allows tests to inject a mock factory
90+
func SetFactory(f ClientFactory) {
91+
Factory = f
92+
}
93+
94+
// ResetFactory resets to the default factory
95+
func ResetFactory() {
96+
Factory = &DefaultFactory{}
97+
}
98+
99+
// CreateClient is a convenience function that uses the global factory
100+
func CreateClient(address, token string) (ConsulClient, error) {
101+
return Factory.NewClient(address, token)
102+
}

pkg/consul/factory_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package consul
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/consul/api"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// MockConsulClient for testing
12+
type MockConsulClient struct {
13+
MockAgent *MockAgent
14+
}
15+
16+
func (m *MockConsulClient) Agent() ConsulAgent {
17+
return m.MockAgent
18+
}
19+
20+
type MockAgent struct {
21+
ServiceFunc func(serviceID string, q *api.QueryOptions) (*api.AgentService, *api.QueryMeta, error)
22+
ServiceRegisterFunc func(reg *api.AgentServiceRegistration) error
23+
}
24+
25+
func (m *MockAgent) Service(serviceID string, q *api.QueryOptions) (*api.AgentService, *api.QueryMeta, error) {
26+
if m.ServiceFunc != nil {
27+
return m.ServiceFunc(serviceID, q)
28+
}
29+
return nil, nil, nil
30+
}
31+
32+
func (m *MockAgent) ServiceRegister(reg *api.AgentServiceRegistration) error {
33+
if m.ServiceRegisterFunc != nil {
34+
return m.ServiceRegisterFunc(reg)
35+
}
36+
return nil
37+
}
38+
39+
func TestDefaultFactory(t *testing.T) {
40+
factory := &DefaultFactory{}
41+
42+
// Test with valid configuration
43+
// Note: This will actually try to connect to Consul, so it might fail
44+
// in environments without Consul running
45+
t.Run("Create client with valid config", func(t *testing.T) {
46+
client, err := factory.NewClient("127.0.0.1:8500", "test-token")
47+
// We expect this to succeed in creating a client object,
48+
// even if Consul isn't actually running
49+
assert.NoError(t, err)
50+
assert.NotNil(t, client)
51+
})
52+
}
53+
54+
func TestMockFactory(t *testing.T) {
55+
t.Run("Returns mock client", func(t *testing.T) {
56+
mockClient := &MockConsulClient{
57+
MockAgent: &MockAgent{},
58+
}
59+
factory := &MockFactory{
60+
MockClient: mockClient,
61+
}
62+
63+
client, err := factory.NewClient("any-address", "any-token")
64+
assert.NoError(t, err)
65+
assert.Equal(t, mockClient, client)
66+
})
67+
68+
t.Run("Returns error when configured", func(t *testing.T) {
69+
expectedErr := fmt.Errorf("connection failed")
70+
factory := &MockFactory{
71+
MockError: expectedErr,
72+
}
73+
74+
client, err := factory.NewClient("any-address", "any-token")
75+
assert.Error(t, err)
76+
assert.Equal(t, expectedErr, err)
77+
assert.Nil(t, client)
78+
})
79+
}
80+
81+
func TestGlobalFactory(t *testing.T) {
82+
// Save original factory
83+
originalFactory := Factory
84+
defer func() {
85+
Factory = originalFactory
86+
}()
87+
88+
t.Run("Default factory is set", func(t *testing.T) {
89+
ResetFactory()
90+
_, ok := Factory.(*DefaultFactory)
91+
assert.True(t, ok, "Default factory should be DefaultFactory type")
92+
})
93+
94+
t.Run("Can set mock factory", func(t *testing.T) {
95+
mockFactory := &MockFactory{
96+
MockClient: &MockConsulClient{},
97+
}
98+
SetFactory(mockFactory)
99+
assert.Equal(t, mockFactory, Factory)
100+
})
101+
102+
t.Run("CreateClient uses global factory", func(t *testing.T) {
103+
mockClient := &MockConsulClient{
104+
MockAgent: &MockAgent{},
105+
}
106+
mockFactory := &MockFactory{
107+
MockClient: mockClient,
108+
}
109+
SetFactory(mockFactory)
110+
111+
client, err := CreateClient("test-addr", "test-token")
112+
assert.NoError(t, err)
113+
assert.Equal(t, mockClient, client)
114+
})
115+
116+
t.Run("ResetFactory restores default", func(t *testing.T) {
117+
mockFactory := &MockFactory{}
118+
SetFactory(mockFactory)
119+
ResetFactory()
120+
_, ok := Factory.(*DefaultFactory)
121+
assert.True(t, ok, "Factory should be reset to DefaultFactory")
122+
})
123+
}

0 commit comments

Comments
 (0)