catwalk_test.go

  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}