Skip to content

Commit b8a6978

Browse files
author
Mauricio Bonetti
committed
issues/1449 - Add comprehensive tests for composite keyring provider functionality
1 parent 6dcbe92 commit b8a6978

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

pkg/secrets/keyring/composite_test.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package keyring
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// mockProvider is a test implementation of the Provider interface
12+
type mockProvider struct {
13+
name string
14+
available bool
15+
setErr error
16+
getErr error
17+
deleteErr error
18+
storage map[string]map[string]string // service -> key -> value
19+
}
20+
21+
func newMockProvider(name string, available bool) *mockProvider {
22+
return &mockProvider{
23+
name: name,
24+
available: available,
25+
storage: make(map[string]map[string]string),
26+
}
27+
}
28+
29+
func (m *mockProvider) Set(service, key, value string) error {
30+
if m.setErr != nil {
31+
return m.setErr
32+
}
33+
if m.storage[service] == nil {
34+
m.storage[service] = make(map[string]string)
35+
}
36+
m.storage[service][key] = value
37+
return nil
38+
}
39+
40+
func (m *mockProvider) Get(service, key string) (string, error) {
41+
if m.getErr != nil {
42+
return "", m.getErr
43+
}
44+
if serviceMap, exists := m.storage[service]; exists {
45+
if value, exists := serviceMap[key]; exists {
46+
return value, nil
47+
}
48+
}
49+
return "", ErrNotFound
50+
}
51+
52+
func (m *mockProvider) Delete(service, key string) error {
53+
if m.deleteErr != nil {
54+
return m.deleteErr
55+
}
56+
if serviceMap, exists := m.storage[service]; exists {
57+
delete(serviceMap, key)
58+
if len(serviceMap) == 0 {
59+
delete(m.storage, service)
60+
}
61+
}
62+
return nil
63+
}
64+
65+
func (m *mockProvider) DeleteAll(service string) error {
66+
delete(m.storage, service)
67+
return nil
68+
}
69+
70+
func (m *mockProvider) IsAvailable() bool {
71+
return m.available
72+
}
73+
74+
func (m *mockProvider) Name() string {
75+
return m.name
76+
}
77+
78+
func TestNewCompositeProvider(t *testing.T) {
79+
t.Parallel()
80+
provider := NewCompositeProvider()
81+
require.NotNil(t, provider)
82+
83+
composite, ok := provider.(*compositeProvider)
84+
require.True(t, ok, "provider should be a compositeProvider")
85+
86+
// Should always have at least one provider (zalando wrapper)
87+
assert.GreaterOrEqual(t, len(composite.providers), 1)
88+
89+
// First provider should always be zalando wrapper
90+
firstProvider := composite.providers[0]
91+
require.NotNil(t, firstProvider)
92+
93+
// Test platform-specific behavior
94+
switch runtime.GOOS {
95+
case "linux":
96+
// On Linux, first provider should be D-Bus Secret Service
97+
assert.Equal(t, "D-Bus Secret Service", firstProvider.Name())
98+
// On Linux, we might have keyctl as fallback (if available)
99+
// Length could be 1 (only zalando) or 2 (zalando + keyctl)
100+
assert.GreaterOrEqual(t, len(composite.providers), 1)
101+
assert.LessOrEqual(t, len(composite.providers), 2)
102+
103+
if len(composite.providers) == 2 {
104+
// If keyctl is available, it should be second
105+
assert.Equal(t, "Linux Keyctl", composite.providers[1].Name())
106+
}
107+
case "darwin":
108+
// On macOS, should have macOS Keychain
109+
assert.Equal(t, "macOS Keychain", firstProvider.Name())
110+
// Should have exactly one provider on macOS
111+
assert.Equal(t, 1, len(composite.providers))
112+
case "windows":
113+
// On Windows, should have Windows Credential Manager
114+
assert.Equal(t, "Windows Credential Manager", firstProvider.Name())
115+
// Should have exactly one provider on Windows
116+
assert.Equal(t, 1, len(composite.providers))
117+
default:
118+
// On other platforms, should have generic name
119+
assert.Equal(t, "Platform Keyring", firstProvider.Name())
120+
}
121+
122+
// Verify the composite provider implements all interface methods
123+
assert.NotNil(t, composite.IsAvailable)
124+
assert.NotNil(t, composite.Name)
125+
assert.NotNil(t, composite.Get)
126+
assert.NotNil(t, composite.Set)
127+
assert.NotNil(t, composite.Delete)
128+
assert.NotNil(t, composite.DeleteAll)
129+
}
130+
131+
func TestCompositeProvider_GetActiveProvider(t *testing.T) {
132+
t.Parallel()
133+
tests := []struct {
134+
name string
135+
primaryAvailable bool
136+
secondaryAvailable bool
137+
expectedProvider string
138+
expectedNil bool
139+
}{
140+
{
141+
name: "primary available, use primary",
142+
primaryAvailable: true,
143+
secondaryAvailable: true,
144+
expectedProvider: "primary",
145+
expectedNil: false,
146+
},
147+
{
148+
name: "primary unavailable, use secondary",
149+
primaryAvailable: false,
150+
secondaryAvailable: true,
151+
expectedProvider: "secondary",
152+
expectedNil: false,
153+
},
154+
{
155+
name: "both unavailable, return nil",
156+
primaryAvailable: false,
157+
secondaryAvailable: false,
158+
expectedProvider: "",
159+
expectedNil: true,
160+
},
161+
{
162+
name: "only primary available",
163+
primaryAvailable: true,
164+
secondaryAvailable: false,
165+
expectedProvider: "primary",
166+
expectedNil: false,
167+
},
168+
}
169+
170+
for _, tt := range tests {
171+
t.Run(tt.name, func(t *testing.T) {
172+
t.Parallel()
173+
primary := newMockProvider("primary", tt.primaryAvailable)
174+
secondary := newMockProvider("secondary", tt.secondaryAvailable)
175+
176+
composite := &compositeProvider{
177+
providers: []Provider{primary, secondary},
178+
}
179+
180+
activeProvider := composite.getActiveProvider()
181+
182+
if tt.expectedNil {
183+
assert.Nil(t, activeProvider)
184+
} else {
185+
require.NotNil(t, activeProvider)
186+
assert.Equal(t, tt.expectedProvider, activeProvider.Name())
187+
// Verify the active provider is cached
188+
assert.Equal(t, activeProvider, composite.active)
189+
}
190+
})
191+
}
192+
}
193+
194+
func TestCompositeProvider_Operations_WithAvailableProvider(t *testing.T) {
195+
t.Parallel()
196+
mockProv := newMockProvider("test-provider", true)
197+
composite := &compositeProvider{
198+
providers: []Provider{mockProv},
199+
}
200+
201+
// Test Set
202+
err := composite.Set("test-service", "test-key", "test-value")
203+
assert.NoError(t, err)
204+
205+
// Test Get
206+
value, err := composite.Get("test-service", "test-key")
207+
assert.NoError(t, err)
208+
assert.Equal(t, "test-value", value)
209+
210+
// Test Delete
211+
err = composite.Delete("test-service", "test-key")
212+
assert.NoError(t, err)
213+
214+
// Verify deletion
215+
_, err = composite.Get("test-service", "test-key")
216+
assert.ErrorIs(t, err, ErrNotFound)
217+
218+
// Test DeleteAll
219+
_ = composite.Set("test-service", "key1", "value1")
220+
_ = composite.Set("test-service", "key2", "value2")
221+
err = composite.DeleteAll("test-service")
222+
assert.NoError(t, err)
223+
}
224+
225+
func TestCompositeProvider_Operations_NoProviderAvailable(t *testing.T) {
226+
t.Parallel()
227+
mockProv := newMockProvider("test-provider", false)
228+
composite := &compositeProvider{
229+
providers: []Provider{mockProv},
230+
}
231+
232+
// Test Set
233+
err := composite.Set("test-service", "test-key", "test-value")
234+
assert.Error(t, err)
235+
assert.Contains(t, err.Error(), "no keyring provider available")
236+
237+
// Test Get
238+
_, err = composite.Get("test-service", "test-key")
239+
assert.Error(t, err)
240+
assert.Contains(t, err.Error(), "no keyring provider available")
241+
242+
// Test Delete
243+
err = composite.Delete("test-service", "test-key")
244+
assert.Error(t, err)
245+
assert.Contains(t, err.Error(), "no keyring provider available")
246+
247+
// Test DeleteAll
248+
err = composite.DeleteAll("test-service")
249+
assert.Error(t, err)
250+
assert.Contains(t, err.Error(), "no keyring provider available")
251+
}
252+
253+
// Integration test with actual runtime behavior
254+
func TestCompositeProvider_RealProviders(t *testing.T) {
255+
t.Parallel()
256+
provider := NewCompositeProvider()
257+
require.NotNil(t, provider)
258+
259+
// Should always have at least one provider (zalando wrapper)
260+
composite, ok := provider.(*compositeProvider)
261+
require.True(t, ok)
262+
assert.GreaterOrEqual(t, len(composite.providers), 1)
263+
264+
// Test that the provider selection works
265+
_ = composite.getActiveProvider()
266+
267+
// On any platform, we should get some kind of provider name
268+
name := provider.Name()
269+
assert.NotEmpty(t, name)
270+
assert.NotEqual(t, "None Available", name, "should have at least one working provider")
271+
272+
// Test basic availability
273+
available := provider.IsAvailable()
274+
if available {
275+
// If available, basic operations should work
276+
err := provider.Set("toolhive-test", "integration-test", "test-value")
277+
if err == nil {
278+
// Only test Get/Delete if Set worked
279+
value, err := provider.Get("toolhive-test", "integration-test")
280+
if err == nil {
281+
assert.Equal(t, "test-value", value)
282+
}
283+
_ = provider.Delete("toolhive-test", "integration-test")
284+
}
285+
}
286+
}

0 commit comments

Comments
 (0)