@@ -1,10 +1,9 @@
11package config
22
33import (
4− "context"
54 "encoding/json"
6− "errors"
75 "os"
6+ "path/filepath"
87 "sync"
98 "testing"
109
@@ -12,47 +11,31 @@ import (
1211 "github.com/stretchr/testify/require"
1312)
1413
15−type mockProviderClient struct {
16− shouldFail bool
17− shouldReturnErr error
18−}
19−
20−func (m *mockProviderClient) GetProviders(context.Context, string) ([]catwalk.Provider, error) {
21− if m.shouldReturnErr != nil {
22− return nil, m.shouldReturnErr
23− }
24− if m.shouldFail {
25− return nil, errors.New("failed to load providers")
26− }
27− return []catwalk.Provider{
28− {
29− Name: "Mock",
30− },
31− }, nil
32−}
33−
3414func resetProviderState() {
3515 providerOnce = sync.Once{}
3616 providerList = nil
3717 providerErr = nil
18+ catwalkSyncer = &catwalkSync{}
19+ hyperSyncer = &hyperSync{}
3820}
3921
40−func TestProvider_loadProvidersNoIssues(t *testing.T) {
41− client := &mockProviderClient{shouldFail: false}
42− tmpPath := t.TempDir() + "/providers.json"
43− providers, err := loadProviders(client, "", tmpPath)
44− require.NoError(t, err)
45− require.NotNil(t, providers)
46− require.Len(t, providers, 1)
22+func TestProviders_Integration_AutoUpdateDisabled(t *testing.T) {
23+ tmpDir := t.TempDir()
24+ t.Setenv("XDG_DATA_HOME", tmpDir)
4725
48− // check if file got saved
49− fileInfo, err := os.Stat(tmpPath)
50− require.NoError(t, err)
51− require.False(t, fileInfo.IsDir(), "Expected a file, not a directory")
52−}
26+ // Use a test-specific instance to avoid global state interference.
27+ testCatwalkSyncer := &catwalkSync{}
28+ testHyperSyncer := &hyperSync{}
29+
30+ originalCatwalSyncer := catwalkSyncer
31+ originalHyperSyncer := hyperSyncer
32+ defer func() {
33+ catwalkSyncer = originalCatwalSyncer
34+ hyperSyncer = originalHyperSyncer
35+ }()
5336
54−func TestProvider_DisableAutoUpdate(t *testing.T) {
55− t.Setenv("XDG_DATA_HOME", t.TempDir())
37+ catwalkSyncer = testCatwalkSyncer
38+ hyperSyncer = testHyperSyncer
5639
5740 resetProviderState()
5841 defer resetProviderState()
@@ -69,76 +52,257 @@ func TestProvider_DisableAutoUpdate(t *testing.T) {
6952 require.Greater(t, len(providers), 5, "Expected embedded providers")
7053}
7154
72−func TestProvider_WithValidCache(t *testing.T) {
55+func TestProviders_Integration_WithMockClients(t *testing.T) {
7356 tmpDir := t.TempDir()
7457 t.Setenv("XDG_DATA_HOME", tmpDir)
7558
76− resetProviderState()
77− defer resetProviderState()
59+ // Create fresh syncers for this test.
60+ testCatwalkSyncer := &catwalkSync{}
61+ testHyperSyncer := &hyperSync{}
62+
63+ // Initialize with mock clients.
64+ mockCatwalkClient := &mockCatwalkClient{
65+ providers: []catwalk.Provider{
66+ {Name: "Provider1", ID: "p1"},
67+ {Name: "Provider2", ID: "p2"},
68+ },
69+ }
70+ mockHyperClient := &mockHyperClient{
71+ provider: catwalk.Provider{
72+ Name: "Hyper",
73+ ID: "hyper",
74+ Models: []catwalk.Model{
75+ {ID: "hyper-1", Name: "Hyper Model"},
76+ },
77+ },
78+ }
79+
80+ catwalkPath := tmpDir + "/crush/providers.json"
81+ hyperPath := tmpDir + "/crush/hyper.json"
82+
83+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
84+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
85+
86+ // Get providers from each syncer.
87+ catwalkProviders, err := testCatwalkSyncer.Get(t.Context())
88+ require.NoError(t, err)
89+ require.Len(t, catwalkProviders, 2)
90+
91+ hyperProvider, err := testHyperSyncer.Get(t.Context())
92+ require.NoError(t, err)
93+ require.Equal(t, "Hyper", hyperProvider.Name)
94+
95+ // Verify total.
96+ allProviders := append(catwalkProviders, hyperProvider)
97+ require.Len(t, allProviders, 3)
98+}
99+
100+func TestProviders_Integration_WithCachedData(t *testing.T) {
101+ tmpDir := t.TempDir()
102+ t.Setenv("XDG_DATA_HOME", tmpDir)
103+
104+ // Create cache files.
105+ catwalkPath := tmpDir + "/crush/providers.json"
106+ hyperPath := tmpDir + "/crush/hyper.json"
78107
79− cachePath := tmpDir + "/crush/providers.json"
80108 require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
81− cachedProviders := []catwalk.Provider{
82− {Name: "Cached"},
109+
110+ // Write Catwalk cache.
111+ catwalkProviders := []catwalk.Provider{
112+ {Name: "Cached1", ID: "c1"},
113+ {Name: "Cached2", ID: "c2"},
114+ }
115+ data, err := json.Marshal(catwalkProviders)
116+ require.NoError(t, err)
117+ require.NoError(t, os.WriteFile(catwalkPath, data, 0o644))
118+
119+ // Write Hyper cache.
120+ hyperProvider := catwalk.Provider{
121+ Name: "Cached Hyper",
122+ ID: "hyper",
83123 }
84− data, err := json.Marshal(cachedProviders)
124+ data, err = json.Marshal(hyperProvider)
85125 require.NoError(t, err)
86− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
126+ require.NoError(t, os.WriteFile(hyperPath, data, 0o644))
127+
128+ // Create fresh syncers.
129+ testCatwalkSyncer := &catwalkSync{}
130+ testHyperSyncer := &hyperSync{}
131+
132+ // Mock clients that return ErrNotModified.
133+ mockCatwalkClient := &mockCatwalkClient{
134+ err: catwalk.ErrNotModified,
135+ }
136+ mockHyperClient := &mockHyperClient{
137+ err: catwalk.ErrNotModified,
138+ }
87139
88− mockClient := &mockProviderClient{shouldFail: false}
140+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
141+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
89142
90− providers, err := loadProviders(mockClient, "", cachePath)
143+ // Get providers - should use cached.
144+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
91145 require.NoError(t, err)
92− require.NotNil(t, providers)
93− require.Len(t, providers, 1)
94− require.Equal(t, "Mock", providers[0].Name, "Expected fresh provider from fetch")
146+ require.Len(t, catwalkResult, 2)
147+ require.Equal(t, "Cached1", catwalkResult[0].Name)
148+
149+ hyperResult, err := testHyperSyncer.Get(t.Context())
150+ require.NoError(t, err)
151+ require.Equal(t, "Cached Hyper", hyperResult.Name)
95152}
96153
97−func TestProvider_NotModifiedUsesCached(t *testing.T) {
154+func TestProviders_Integration_CatwalkFailsHyperSucceeds(t *testing.T) {
98155 tmpDir := t.TempDir()
99156 t.Setenv("XDG_DATA_HOME", tmpDir)
100157
101− resetProviderState()
102− defer resetProviderState()
158+ testCatwalkSyncer := &catwalkSync{}
159+ testHyperSyncer := &hyperSync{}
103160
104− cachePath := tmpDir + "/crush/providers.json"
105− require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
106− cachedProviders := []catwalk.Provider{
107− {Name: "Cached"},
161+ // Catwalk fails, Hyper succeeds.
162+ mockCatwalkClient := &mockCatwalkClient{
163+ err: catwalk.ErrNotModified, // Will use embedded.
108164 }
109− data, err := json.Marshal(cachedProviders)
165+ mockHyperClient := &mockHyperClient{
166+ provider: catwalk.Provider{
167+ Name: "Hyper",
168+ ID: "hyper",
169+ Models: []catwalk.Model{
170+ {ID: "hyper-1", Name: "Hyper Model"},
171+ },
172+ },
173+ }
174+
175+ catwalkPath := tmpDir + "/crush/providers.json"
176+ hyperPath := tmpDir + "/crush/hyper.json"
177+
178+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
179+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
180+
181+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
110182 require.NoError(t, err)
111− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
183+ require.NotEmpty(t, catwalkResult) // Should have embedded.
112184
113− mockClient := &mockProviderClient{shouldReturnErr: catwalk.ErrNotModified}
114− providers, err := loadProviders(mockClient, "", cachePath)
115− require.ErrorIs(t, err, catwalk.ErrNotModified)
116− require.Nil(t, providers)
185+ hyperResult, err := testHyperSyncer.Get(t.Context())
186+ require.NoError(t, err)
187+ require.Equal(t, "Hyper", hyperResult.Name)
117188}
118189
119−func TestProvider_EmptyCacheDefaultsToEmbedded(t *testing.T) {
190+func TestProviders_Integration_BothFail(t *testing.T) {
120191 tmpDir := t.TempDir()
121192 t.Setenv("XDG_DATA_HOME", tmpDir)
122193
123− resetProviderState()
124− defer resetProviderState()
194+ testCatwalkSyncer := &catwalkSync{}
195+ testHyperSyncer := &hyperSync{}
125196
126− cachePath := tmpDir + "/crush/providers.json"
127− require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
128− emptyProviders := []catwalk.Provider{}
129− data, err := json.Marshal(emptyProviders)
197+ // Both fail.
198+ mockCatwalkClient := &mockCatwalkClient{
199+ err: catwalk.ErrNotModified,
200+ }
201+ mockHyperClient := &mockHyperClient{
202+ provider: catwalk.Provider{}, // Empty provider.
203+ }
204+
205+ catwalkPath := tmpDir + "/crush/providers.json"
206+ hyperPath := tmpDir + "/crush/hyper.json"
207+
208+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
209+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
210+
211+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
130212 require.NoError(t, err)
131− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
213+ require.NotEmpty(t, catwalkResult) // Should fall back to embedded.
132214
133− cached, _, err := loadProvidersFromCache(cachePath)
215+ hyperResult, err := testHyperSyncer.Get(t.Context())
134216 require.NoError(t, err)
135− require.Empty(t, cached, "Expected empty cache")
217+ require.Equal(t, "Charm Hyper", hyperResult.Name) // Falls back to embedded when no models.
136218}
137219
138−func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
139− client := &mockProviderClient{shouldFail: true}
140− tmpPath := t.TempDir() + "/providers.json"
141− providers, err := loadProviders(client, "", tmpPath)
220+func TestCache_StoreAndGet(t *testing.T) {
221+ t.Parallel()
222+
223+ tmpDir := t.TempDir()
224+ cachePath := tmpDir + "/test.json"
225+
226+ cache := newCache[[]catwalk.Provider](cachePath)
227+
228+ providers := []catwalk.Provider{
229+ {Name: "Provider1", ID: "p1"},
230+ {Name: "Provider2", ID: "p2"},
231+ }
232+
233+ // Store.
234+ err := cache.Store(providers)
235+ require.NoError(t, err)
236+
237+ // Get.
238+ result, etag, err := cache.Get()
239+ require.NoError(t, err)
240+ require.Len(t, result, 2)
241+ require.Equal(t, "Provider1", result[0].Name)
242+ require.NotEmpty(t, etag)
243+}
244+
245+func TestCache_GetNonExistent(t *testing.T) {
246+ t.Parallel()
247+
248+ tmpDir := t.TempDir()
249+ cachePath := tmpDir + "/nonexistent.json"
250+
251+ cache := newCache[[]catwalk.Provider](cachePath)
252+
253+ _, _, err := cache.Get()
142254 require.Error(t, err)
143− require.Nil(t, providers, "Expected nil providers when loading fails and no cache exists")
255+ require.Contains(t, err.Error(), "failed to read provider cache file")
256+}
257+
258+func TestCache_GetInvalidJSON(t *testing.T) {
259+ t.Parallel()
260+
261+ tmpDir := t.TempDir()
262+ cachePath := tmpDir + "/invalid.json"
263+
264+ require.NoError(t, os.WriteFile(cachePath, []byte("invalid json"), 0o644))
265+
266+ cache := newCache[[]catwalk.Provider](cachePath)
267+
268+ _, _, err := cache.Get()
269+ require.Error(t, err)
270+ require.Contains(t, err.Error(), "failed to unmarshal provider data from cache")
271+}
272+
273+func TestCachePathFor(t *testing.T) {
274+ tests := []struct {
275+ name string
276+ xdgDataHome string
277+ expected string
278+ }{
279+ {
280+ name: "with XDG_DATA_HOME",
281+ xdgDataHome: "/custom/data",
282+ expected: "/custom/data/crush/providers.json",
283+ },
284+ {
285+ name: "without XDG_DATA_HOME",
286+ xdgDataHome: "",
287+ expected: "", // Will use platform-specific default.
288+ },
289+ }
290+
291+ for _, tt := range tests {
292+ t.Run(tt.name, func(t *testing.T) {
293+ if tt.xdgDataHome != "" {
294+ t.Setenv("XDG_DATA_HOME", tt.xdgDataHome)
295+ } else {
296+ t.Setenv("XDG_DATA_HOME", "")
297+ }
298+
299+ result := cachePathFor("providers")
300+ if tt.expected != "" {
301+ require.Equal(t, tt.expected, filepath.ToSlash(result))
302+ } else {
303+ require.Contains(t, result, "crush")
304+ require.Contains(t, result, "providers.json")
305+ }
306+ })
307+ }
144308}