1package config
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "testing"
9
10 "github.com/charmbracelet/catwalk/pkg/catwalk"
11 "github.com/stretchr/testify/require"
12)
13
14type mockCatwalkClient struct {
15 providers []catwalk.Provider
16 err error
17 callCount int
18}
19
20func (m *mockCatwalkClient) GetProviders(ctx context.Context, etag string) ([]catwalk.Provider, error) {
21 m.callCount++
22 return m.providers, m.err
23}
24
25func TestCatwalkSync_Init(t *testing.T) {
26 t.Parallel()
27
28 syncer := &catwalkSync{}
29 client := &mockCatwalkClient{}
30 path := "/tmp/test.json"
31
32 syncer.Init(client, path, true)
33
34 require.True(t, syncer.init.Load())
35 require.Equal(t, client, syncer.client)
36 require.Equal(t, path, syncer.cache.path)
37 require.True(t, syncer.autoupdate)
38}
39
40func TestCatwalkSync_GetPanicIfNotInit(t *testing.T) {
41 t.Parallel()
42
43 syncer := &catwalkSync{}
44 require.Panics(t, func() {
45 _, _ = syncer.Get(t.Context())
46 })
47}
48
49func TestCatwalkSync_GetWithAutoUpdateDisabled(t *testing.T) {
50 t.Parallel()
51
52 syncer := &catwalkSync{}
53 client := &mockCatwalkClient{
54 providers: []catwalk.Provider{{Name: "should-not-be-used"}},
55 }
56 path := t.TempDir() + "/providers.json"
57
58 syncer.Init(client, path, false)
59
60 providers, err := syncer.Get(t.Context())
61 require.NoError(t, err)
62 require.NotEmpty(t, providers)
63 require.Equal(t, 0, client.callCount, "Client should not be called when autoupdate is disabled")
64
65 // Should return embedded providers.
66 for _, p := range providers {
67 require.NotEqual(t, "should-not-be-used", p.Name)
68 }
69}
70
71func TestCatwalkSync_GetFreshProviders(t *testing.T) {
72 t.Parallel()
73
74 syncer := &catwalkSync{}
75 client := &mockCatwalkClient{
76 providers: []catwalk.Provider{
77 {Name: "Fresh Provider", ID: "fresh"},
78 },
79 }
80 path := t.TempDir() + "/providers.json"
81
82 syncer.Init(client, path, true)
83
84 providers, err := syncer.Get(t.Context())
85 require.NoError(t, err)
86 require.Len(t, providers, 1)
87 require.Equal(t, "Fresh Provider", providers[0].Name)
88 require.Equal(t, 1, client.callCount)
89
90 // Verify cache was written.
91 fileInfo, err := os.Stat(path)
92 require.NoError(t, err)
93 require.False(t, fileInfo.IsDir())
94}
95
96func TestCatwalkSync_GetNotModifiedUsesCached(t *testing.T) {
97 t.Parallel()
98
99 tmpDir := t.TempDir()
100 path := tmpDir + "/providers.json"
101
102 // Create cache file.
103 cachedProviders := []catwalk.Provider{
104 {Name: "Cached Provider", ID: "cached"},
105 }
106 data, err := json.Marshal(cachedProviders)
107 require.NoError(t, err)
108 require.NoError(t, os.WriteFile(path, data, 0o644))
109
110 syncer := &catwalkSync{}
111 client := &mockCatwalkClient{
112 err: catwalk.ErrNotModified,
113 }
114
115 syncer.Init(client, path, true)
116
117 providers, err := syncer.Get(t.Context())
118 require.NoError(t, err)
119 require.Len(t, providers, 1)
120 require.Equal(t, "Cached Provider", providers[0].Name)
121 require.Equal(t, 1, client.callCount)
122}
123
124func TestCatwalkSync_GetEmptyResultFallbackToCached(t *testing.T) {
125 t.Parallel()
126
127 tmpDir := t.TempDir()
128 path := tmpDir + "/providers.json"
129
130 // Create cache file.
131 cachedProviders := []catwalk.Provider{
132 {Name: "Cached Provider", ID: "cached"},
133 }
134 data, err := json.Marshal(cachedProviders)
135 require.NoError(t, err)
136 require.NoError(t, os.WriteFile(path, data, 0o644))
137
138 syncer := &catwalkSync{}
139 client := &mockCatwalkClient{
140 providers: []catwalk.Provider{}, // Empty result.
141 }
142
143 syncer.Init(client, path, true)
144
145 providers, err := syncer.Get(t.Context())
146 require.Error(t, err)
147 require.Contains(t, err.Error(), "empty providers list from catwalk")
148 require.Len(t, providers, 1)
149 require.Equal(t, "Cached Provider", providers[0].Name)
150}
151
152func TestCatwalkSync_GetEmptyCacheDefaultsToEmbedded(t *testing.T) {
153 t.Parallel()
154
155 tmpDir := t.TempDir()
156 path := tmpDir + "/providers.json"
157
158 // Create empty cache file.
159 emptyProviders := []catwalk.Provider{}
160 data, err := json.Marshal(emptyProviders)
161 require.NoError(t, err)
162 require.NoError(t, os.WriteFile(path, data, 0o644))
163
164 syncer := &catwalkSync{}
165 client := &mockCatwalkClient{
166 err: errors.New("network error"),
167 }
168
169 syncer.Init(client, path, true)
170
171 providers, err := syncer.Get(t.Context())
172 require.NoError(t, err)
173 require.NotEmpty(t, providers, "Should fall back to embedded providers")
174
175 // Verify it's embedded providers by checking we have multiple common ones.
176 require.Greater(t, len(providers), 5)
177}
178
179func TestCatwalkSync_GetClientError(t *testing.T) {
180 t.Parallel()
181
182 tmpDir := t.TempDir()
183 path := tmpDir + "/providers.json"
184
185 syncer := &catwalkSync{}
186 client := &mockCatwalkClient{
187 err: errors.New("network error"),
188 }
189
190 syncer.Init(client, path, true)
191
192 providers, err := syncer.Get(t.Context())
193 require.NoError(t, err) // Should fall back to embedded.
194 require.NotEmpty(t, providers)
195}
196
197func TestCatwalkSync_GetCalledMultipleTimesUsesOnce(t *testing.T) {
198 t.Parallel()
199
200 syncer := &catwalkSync{}
201 client := &mockCatwalkClient{
202 providers: []catwalk.Provider{
203 {Name: "Provider", ID: "test"},
204 },
205 }
206 path := t.TempDir() + "/providers.json"
207
208 syncer.Init(client, path, true)
209
210 // Call Get multiple times.
211 providers1, err1 := syncer.Get(t.Context())
212 require.NoError(t, err1)
213 require.Len(t, providers1, 1)
214
215 providers2, err2 := syncer.Get(t.Context())
216 require.NoError(t, err2)
217 require.Len(t, providers2, 1)
218
219 // Client should only be called once due to sync.Once.
220 require.Equal(t, 1, client.callCount)
221}