@@ -1,10 +1,9 @@
1package config
1package config
5 "encoding/json"
4 "encoding/json"
@@ -12,47 +11,31 @@ import (
12 "github.com/stretchr/testify/require"
11 "github.com/stretchr/testify/require"
15−type mockProviderClient struct {
17− shouldReturnErr error
20−func (m *mockProviderClient) GetProviders(context.Context, string) ([]catwalk.Provider, error) {
21− if m.shouldReturnErr != nil {
22− return nil, m.shouldReturnErr
25− return nil, errors.New("failed to load providers")
27− return []catwalk.Provider{
34func resetProviderState() {
14func resetProviderState() {
35 providerOnce = sync.Once{}
15 providerOnce = sync.Once{}
36 providerList = nil
16 providerList = nil
37 providerErr = nil
17 providerErr = nil
18+ catwalkSyncer = &catwalkSync{}
19+ hyperSyncer = &hyperSync{}
40−func TestProvider_loadProvidersNoIssues(t *testing.T) {
22+func TestProviders_Integration_AutoUpdateDisabled(t *testing.T) {
41− client := &mockProviderClient{shouldFail: false}
23+ tmpDir := t.TempDir()
42− tmpPath := t.TempDir() + "/providers.json"
24+ t.Setenv("XDG_DATA_HOME", tmpDir)
43− providers, err := loadProviders(client, "", tmpPath)
44− require.NoError(t, err)
45− require.NotNil(t, providers)
46− require.Len(t, providers, 1)
48− // check if file got saved
26+ // Use a test-specific instance to avoid global state interference.
49− fileInfo, err := os.Stat(tmpPath)
27+ testCatwalkSyncer := &catwalkSync{}
50− require.NoError(t, err)
28+ testHyperSyncer := &hyperSync{}
51− require.False(t, fileInfo.IsDir(), "Expected a file, not a directory")
29+
30+ originalCatwalSyncer := catwalkSyncer
31+ originalHyperSyncer := hyperSyncer
33+ catwalkSyncer = originalCatwalSyncer
34+ hyperSyncer = originalHyperSyncer
54−func TestProvider_DisableAutoUpdate(t *testing.T) {
37+ catwalkSyncer = testCatwalkSyncer
55− t.Setenv("XDG_DATA_HOME", t.TempDir())
38+ hyperSyncer = testHyperSyncer
57 resetProviderState()
40 resetProviderState()
58 defer resetProviderState()
41 defer resetProviderState()
@@ -69,76 +52,257 @@ func TestProvider_DisableAutoUpdate(t *testing.T) {
69 require.Greater(t, len(providers), 5, "Expected embedded providers")
52 require.Greater(t, len(providers), 5, "Expected embedded providers")
72−func TestProvider_WithValidCache(t *testing.T) {
55+func TestProviders_Integration_WithMockClients(t *testing.T) {
73 tmpDir := t.TempDir()
56 tmpDir := t.TempDir()
74 t.Setenv("XDG_DATA_HOME", tmpDir)
57 t.Setenv("XDG_DATA_HOME", tmpDir)
76− resetProviderState()
59+ // Create fresh syncers for this test.
77− defer resetProviderState()
60+ testCatwalkSyncer := &catwalkSync{}
61+ testHyperSyncer := &hyperSync{}
63+ // Initialize with mock clients.
64+ mockCatwalkClient := &mockCatwalkClient{
65+ providers: []catwalk.Provider{
66+ {Name: "Provider1", ID: "p1"},
67+ {Name: "Provider2", ID: "p2"},
70+ mockHyperClient := &mockHyperClient{
71+ provider: catwalk.Provider{
74+ Models: []catwalk.Model{
75+ {ID: "hyper-1", Name: "Hyper Model"},
80+ catwalkPath := tmpDir + "/crush/providers.json"
81+ hyperPath := tmpDir + "/crush/hyper.json"
83+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
84+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
86+ // Get providers from each syncer.
87+ catwalkProviders, err := testCatwalkSyncer.Get(t.Context())
88+ require.NoError(t, err)
89+ require.Len(t, catwalkProviders, 2)
91+ hyperProvider, err := testHyperSyncer.Get(t.Context())
92+ require.NoError(t, err)
93+ require.Equal(t, "Hyper", hyperProvider.Name)
96+ allProviders := append(catwalkProviders, hyperProvider)
97+ require.Len(t, allProviders, 3)
100+func TestProviders_Integration_WithCachedData(t *testing.T) {
101+ tmpDir := t.TempDir()
102+ t.Setenv("XDG_DATA_HOME", tmpDir)
104+ // Create cache files.
105+ catwalkPath := tmpDir + "/crush/providers.json"
106+ hyperPath := tmpDir + "/crush/hyper.json"
79− cachePath := tmpDir + "/crush/providers.json"
80 require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
108 require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
110+ // Write Catwalk cache.
81− cachedProviders := []catwalk.Provider{
111+ catwalkProviders := []catwalk.Provider{
112+ {Name: "Cached1", ID: "c1"},
113+ {Name: "Cached2", ID: "c2"},
115+ data, err := json.Marshal(catwalkProviders)
116+ require.NoError(t, err)
117+ require.NoError(t, os.WriteFile(catwalkPath, data, 0o644))
119+ // Write Hyper cache.
120+ hyperProvider := catwalk.Provider{
82− {Name: "Cached"},
121+ Name: "Cached Hyper",
84− data, err := json.Marshal(cachedProviders)
124+ data, err = json.Marshal(hyperProvider)
85 require.NoError(t, err)
125 require.NoError(t, err)
86− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
126+ require.NoError(t, os.WriteFile(hyperPath, data, 0o644))
128+ // Create fresh syncers.
129+ testCatwalkSyncer := &catwalkSync{}
130+ testHyperSyncer := &hyperSync{}
132+ // Mock clients that return ErrNotModified.
133+ mockCatwalkClient := &mockCatwalkClient{
134+ err: catwalk.ErrNotModified,
136+ mockHyperClient := &mockHyperClient{
137+ err: catwalk.ErrNotModified,
88− mockClient := &mockProviderClient{shouldFail: false}
140+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
141+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
90− providers, err := loadProviders(mockClient, "", cachePath)
143+ // Get providers - should use cached.
144+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
91 require.NoError(t, err)
145 require.NoError(t, err)
92− require.NotNil(t, providers)
146+ require.Len(t, catwalkResult, 2)
93− require.Len(t, providers, 1)
147+ require.Equal(t, "Cached1", catwalkResult[0].Name)
94− require.Equal(t, "Mock", providers[0].Name, "Expected fresh provider from fetch")
148+
149+ hyperResult, err := testHyperSyncer.Get(t.Context())
150+ require.NoError(t, err)
151+ require.Equal(t, "Cached Hyper", hyperResult.Name)
97−func TestProvider_NotModifiedUsesCached(t *testing.T) {
154+func TestProviders_Integration_CatwalkFailsHyperSucceeds(t *testing.T) {
98 tmpDir := t.TempDir()
155 tmpDir := t.TempDir()
99 t.Setenv("XDG_DATA_HOME", tmpDir)
156 t.Setenv("XDG_DATA_HOME", tmpDir)
101− resetProviderState()
158+ testCatwalkSyncer := &catwalkSync{}
102− defer resetProviderState()
159+ testHyperSyncer := &hyperSync{}
104− cachePath := tmpDir + "/crush/providers.json"
161+ // Catwalk fails, Hyper succeeds.
105− require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
162+ mockCatwalkClient := &mockCatwalkClient{
106− cachedProviders := []catwalk.Provider{
163+ err: catwalk.ErrNotModified, // Will use embedded.
109− data, err := json.Marshal(cachedProviders)
165+ mockHyperClient := &mockHyperClient{
166+ provider: catwalk.Provider{
169+ Models: []catwalk.Model{
170+ {ID: "hyper-1", Name: "Hyper Model"},
175+ catwalkPath := tmpDir + "/crush/providers.json"
176+ hyperPath := tmpDir + "/crush/hyper.json"
178+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
179+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
181+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
110 require.NoError(t, err)
182 require.NoError(t, err)
111− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
183+ require.NotEmpty(t, catwalkResult) // Should have embedded.
113− mockClient := &mockProviderClient{shouldReturnErr: catwalk.ErrNotModified}
185+ hyperResult, err := testHyperSyncer.Get(t.Context())
114− providers, err := loadProviders(mockClient, "", cachePath)
186+ require.NoError(t, err)
115− require.ErrorIs(t, err, catwalk.ErrNotModified)
187+ require.Equal(t, "Hyper", hyperResult.Name)
116− require.Nil(t, providers)
119−func TestProvider_EmptyCacheDefaultsToEmbedded(t *testing.T) {
190+func TestProviders_Integration_BothFail(t *testing.T) {
120 tmpDir := t.TempDir()
191 tmpDir := t.TempDir()
121 t.Setenv("XDG_DATA_HOME", tmpDir)
192 t.Setenv("XDG_DATA_HOME", tmpDir)
123− resetProviderState()
194+ testCatwalkSyncer := &catwalkSync{}
124− defer resetProviderState()
195+ testHyperSyncer := &hyperSync{}
198+ mockCatwalkClient := &mockCatwalkClient{
199+ err: catwalk.ErrNotModified,
201+ mockHyperClient := &mockHyperClient{
202+ provider: catwalk.Provider{}, // Empty provider.
126− cachePath := tmpDir + "/crush/providers.json"
205+ catwalkPath := tmpDir + "/crush/providers.json"
127− require.NoError(t, os.MkdirAll(tmpDir+"/crush", 0o755))
206+ hyperPath := tmpDir + "/crush/hyper.json"
128− emptyProviders := []catwalk.Provider{}
207+
129− data, err := json.Marshal(emptyProviders)
208+ testCatwalkSyncer.Init(mockCatwalkClient, catwalkPath, true)
209+ testHyperSyncer.Init(mockHyperClient, hyperPath, true)
211+ catwalkResult, err := testCatwalkSyncer.Get(t.Context())
130 require.NoError(t, err)
212 require.NoError(t, err)
131− require.NoError(t, os.WriteFile(cachePath, data, 0o644))
213+ require.NotEmpty(t, catwalkResult) // Should fall back to embedded.
133− cached, _, err := loadProvidersFromCache(cachePath)
215+ hyperResult, err := testHyperSyncer.Get(t.Context())
134 require.NoError(t, err)
216 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.
138−func TestProvider_loadProvidersWithIssuesAndNoCache(t *testing.T) {
220+func TestCache_StoreAndGet(t *testing.T) {
139− client := &mockProviderClient{shouldFail: true}
221+ t.Parallel()
140− tmpPath := t.TempDir() + "/providers.json"
222+
141− providers, err := loadProviders(client, "", tmpPath)
223+ tmpDir := t.TempDir()
224+ cachePath := tmpDir + "/test.json"
226+ cache := newCache[[]catwalk.Provider](cachePath)
228+ providers := []catwalk.Provider{
229+ {Name: "Provider1", ID: "p1"},
230+ {Name: "Provider2", ID: "p2"},
234+ err := cache.Store(providers)
235+ require.NoError(t, err)
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)
245+func TestCache_GetNonExistent(t *testing.T) {
248+ tmpDir := t.TempDir()
249+ cachePath := tmpDir + "/nonexistent.json"
251+ cache := newCache[[]catwalk.Provider](cachePath)
253+ _, _, err := cache.Get()
142 require.Error(t, err)
254 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")
258+func TestCache_GetInvalidJSON(t *testing.T) {
261+ tmpDir := t.TempDir()
262+ cachePath := tmpDir + "/invalid.json"
264+ require.NoError(t, os.WriteFile(cachePath, []byte("invalid json"), 0o644))
266+ cache := newCache[[]catwalk.Provider](cachePath)
268+ _, _, err := cache.Get()
269+ require.Error(t, err)
270+ require.Contains(t, err.Error(), "failed to unmarshal provider data from cache")
273+func TestCachePathFor(t *testing.T) {
280+ name: "with XDG_DATA_HOME",
281+ xdgDataHome: "/custom/data",
282+ expected: "/custom/data/crush/providers.json",
285+ name: "without XDG_DATA_HOME",
287+ expected: "", // Will use platform-specific default.
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)
296+ t.Setenv("XDG_DATA_HOME", "")
299+ result := cachePathFor("providers")
300+ if tt.expected != "" {
301+ require.Equal(t, tt.expected, filepath.ToSlash(result))
303+ require.Contains(t, result, "crush")
304+ require.Contains(t, result, "providers.json")