Skip to content

Commit 8a3af03

Browse files
authored
fix(search): provide graceful warning when a registry is offline (#296)
fixes #295 Changes `dbc search` to succeed with a warning if any of the registries still succeed and return a list of drivers while one or more fail. If there's an error but no drivers at all then we still fail entirely.
1 parent c8aa25f commit 8a3af03

File tree

13 files changed

+593
-44
lines changed

13 files changed

+593
-44
lines changed

cmd/dbc/add.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,14 @@ func (m addModel) Init() tea.Cmd {
9494
}
9595

9696
return func() tea.Msg {
97-
drivers, err := m.getDriverRegistry()
98-
if err != nil {
99-
return fmt.Errorf("error getting driver list: %w", err)
97+
drivers, registryErr := m.getDriverRegistry()
98+
// If we have no drivers and there's an error, fail immediately
99+
if len(drivers) == 0 && registryErr != nil {
100+
return fmt.Errorf("error getting driver list: %w", registryErr)
100101
}
102+
// Store registry errors to use later if driver is not found
103+
// We continue processing if we have some drivers
104+
var registryErrors error = registryErr
101105

102106
p, err := driverListPath(m.Path)
103107
if err != nil {
@@ -130,6 +134,10 @@ func (m addModel) Init() tea.Cmd {
130134

131135
drv, err := findDriver(spec.Name, drivers)
132136
if err != nil {
137+
// If we have registry errors, enhance the error message
138+
if registryErrors != nil {
139+
return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErrors.Error())
140+
}
133141
return err
134142
}
135143

@@ -141,7 +149,12 @@ func (m addModel) Init() tea.Cmd {
141149
}
142150
} else {
143151
if !m.Pre && !drv.HasNonPrerelease() {
144-
return fmt.Errorf("driver `%s` not found in driver registry index", spec.Name)
152+
err := fmt.Errorf("driver `%s` not found in driver registry index", spec.Name)
153+
// If we have registry errors, enhance the error message
154+
if registryErrors != nil {
155+
return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErrors.Error())
156+
}
157+
return err
145158
}
146159
}
147160

cmd/dbc/add_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717
import (
1818
"bytes"
1919
"context"
20+
"fmt"
2021
"os"
2122
"path/filepath"
2223
"testing"
@@ -326,3 +327,85 @@ func (suite *SubcommandTestSuite) TestAddExplicitPrereleaseWithoutPreFlag() {
326327
version = '=0.9.0-alpha.1'
327328
`, string(data))
328329
}
330+
331+
func (suite *SubcommandTestSuite) TestAddPartialRegistryFailure() {
332+
// Initialize driver list
333+
m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel()
334+
suite.runCmd(m)
335+
336+
// Test that add command handles partial registry failure gracefully
337+
// (one registry succeeds, another fails - returns both drivers and error)
338+
partialFailingRegistry := func() ([]dbc.Driver, error) {
339+
// Get drivers from the test registry (simulating one successful registry)
340+
drivers, _ := getTestDriverRegistry()
341+
// But also return an error (simulating another registry that failed)
342+
return drivers, fmt.Errorf("registry https://cdn-fallback.example.com: failed to fetch driver registry: DNS resolution failed")
343+
}
344+
345+
// Should succeed if the requested driver is found in the available drivers
346+
m = AddCmd{
347+
Path: filepath.Join(suite.tempdir, "dbc.toml"),
348+
Driver: []string{"test-driver-1"},
349+
Pre: false,
350+
}.GetModelCustom(
351+
baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg})
352+
353+
suite.runCmd(m)
354+
// Should succeed without printing the registry error
355+
356+
// Verify the file was updated correctly
357+
data, err := os.ReadFile(filepath.Join(suite.tempdir, "dbc.toml"))
358+
suite.Require().NoError(err)
359+
suite.Contains(string(data), "[drivers.test-driver-1]")
360+
}
361+
362+
func (suite *SubcommandTestSuite) TestAddPartialRegistryFailureDriverNotFound() {
363+
// Initialize driver list
364+
m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel()
365+
suite.runCmd(m)
366+
367+
// Test that add command shows registry errors when the requested driver is not found
368+
partialFailingRegistry := func() ([]dbc.Driver, error) {
369+
// Get drivers from the test registry (simulating one successful registry)
370+
drivers, _ := getTestDriverRegistry()
371+
// But also return an error (simulating another registry that failed)
372+
return drivers, fmt.Errorf("registry https://cdn-fallback.example.com: failed to fetch driver registry: DNS resolution failed")
373+
}
374+
375+
// Should fail with enhanced error message if the requested driver is not found
376+
m = AddCmd{
377+
Path: filepath.Join(suite.tempdir, "dbc.toml"),
378+
Driver: []string{"nonexistent-driver"},
379+
Pre: false,
380+
}.GetModelCustom(
381+
baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg})
382+
383+
out := suite.runCmdErr(m)
384+
// Should show the driver not found error AND the registry error
385+
suite.Contains(out, "driver `nonexistent-driver` not found")
386+
suite.Contains(out, "Note: Some driver registries were unavailable")
387+
suite.Contains(out, "failed to fetch driver registry")
388+
suite.Contains(out, "DNS resolution failed")
389+
}
390+
391+
func (suite *SubcommandTestSuite) TestAddCompleteRegistryFailure() {
392+
// Initialize driver list
393+
m := InitCmd{Path: filepath.Join(suite.tempdir, "dbc.toml")}.GetModel()
394+
suite.runCmd(m)
395+
396+
// Test that add command handles complete registry failure (no drivers returned)
397+
completeFailingRegistry := func() ([]dbc.Driver, error) {
398+
return nil, fmt.Errorf("registry https://primary-cdn.example.com: network unreachable")
399+
}
400+
401+
m = AddCmd{
402+
Path: filepath.Join(suite.tempdir, "dbc.toml"),
403+
Driver: []string{"test-driver-1"},
404+
Pre: false,
405+
}.GetModelCustom(
406+
baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg})
407+
408+
out := suite.runCmdErr(m)
409+
suite.Contains(out, "error getting driver list")
410+
suite.Contains(out, "network unreachable")
411+
}

cmd/dbc/docs.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ func (c DocsCmd) GetModel() tea.Model {
6767
type docsModel struct {
6868
baseModel
6969

70-
driver string
71-
drv *dbc.Driver
72-
urlToOpen string
73-
noOpen bool
74-
fallbackUrls map[string]string
75-
openBrowser func(string) error
70+
driver string
71+
drv *dbc.Driver
72+
urlToOpen string
73+
noOpen bool
74+
fallbackUrls map[string]string
75+
openBrowser func(string) error
76+
registryErrors error // Store registry errors for better error messages
7677
}
7778

7879
func (m docsModel) Init() tea.Cmd {
@@ -81,13 +82,18 @@ func (m docsModel) Init() tea.Cmd {
8182
return docsUrlFound(dbcDocsUrl)
8283
}
8384

84-
drivers, err := m.getDriverRegistry()
85-
if err != nil {
86-
return err
85+
drivers, registryErr := m.getDriverRegistry()
86+
// If we have no drivers and there's an error, fail immediately
87+
if len(drivers) == 0 && registryErr != nil {
88+
return fmt.Errorf("error getting driver list: %w", registryErr)
8789
}
8890

8991
drv, err := findDriver(m.driver, drivers)
9092
if err != nil {
93+
// If we have registry errors, enhance the error message
94+
if registryErr != nil {
95+
return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErr.Error())
96+
}
9197
return err
9298
}
9399

cmd/dbc/docs_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package main
1616

1717
import (
1818
"fmt"
19+
20+
"github.com/columnar-tech/dbc"
1921
)
2022

2123
var testFallbackUrls = map[string]string{
@@ -154,3 +156,80 @@ func (suite *SubcommandTestSuite) TestDocsDriverFoundWithDocs() {
154156

155157
suite.Equal("http://example.com", lastOpenedURL)
156158
}
159+
160+
func (suite *SubcommandTestSuite) TestDocsPartialRegistryFailure() {
161+
// Test that docs command handles partial registry failure gracefully
162+
// (one registry succeeds, another fails - returns both drivers and error)
163+
partialFailingRegistry := func() ([]dbc.Driver, error) {
164+
// Get drivers from the test registry (simulating one successful registry)
165+
drivers, _ := getTestDriverRegistry()
166+
// But also return an error (simulating another registry that failed)
167+
return drivers, fmt.Errorf("registry https://fallback-registry.example.com: failed to fetch driver registry: timeout")
168+
}
169+
170+
openBrowserFunc = mockOpenBrowserSuccess
171+
lastOpenedURL = ""
172+
fallbackDriverDocsUrl = testFallbackUrls
173+
174+
// Should succeed if the requested driver is found in the available drivers
175+
m := DocsCmd{Driver: "test-driver-1"}.GetModelCustom(
176+
baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg},
177+
false,
178+
mockOpenBrowserSuccess,
179+
testFallbackUrls,
180+
)
181+
182+
suite.runCmd(m)
183+
// Should open docs successfully without showing the registry error
184+
suite.Equal("https://test.example.com/driver1", lastOpenedURL)
185+
}
186+
187+
func (suite *SubcommandTestSuite) TestDocsPartialRegistryFailureDriverNotFound() {
188+
// Test that docs command shows registry errors when the requested driver is not found
189+
partialFailingRegistry := func() ([]dbc.Driver, error) {
190+
// Get drivers from the test registry (simulating one successful registry)
191+
drivers, _ := getTestDriverRegistry()
192+
// But also return an error (simulating another registry that failed)
193+
return drivers, fmt.Errorf("registry https://fallback-registry.example.com: failed to fetch driver registry: timeout")
194+
}
195+
196+
openBrowserFunc = mockOpenBrowserSuccess
197+
lastOpenedURL = ""
198+
199+
// Should fail with enhanced error message if the requested driver is not found
200+
m := DocsCmd{Driver: "nonexistent-driver"}.GetModelCustom(
201+
baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg},
202+
false,
203+
mockOpenBrowserSuccess,
204+
testFallbackUrls,
205+
)
206+
207+
out := suite.runCmdErr(m)
208+
// Should show the driver not found error AND the registry error
209+
suite.Contains(out, "driver `nonexistent-driver` not found")
210+
suite.Contains(out, "Note: Some driver registries were unavailable")
211+
suite.Contains(out, "failed to fetch driver registry")
212+
suite.Contains(out, "timeout")
213+
suite.Equal("", lastOpenedURL, "browser should not be opened on error")
214+
}
215+
216+
func (suite *SubcommandTestSuite) TestDocsCompleteRegistryFailure() {
217+
// Test that docs command handles complete registry failure (no drivers returned)
218+
completeFailingRegistry := func() ([]dbc.Driver, error) {
219+
return nil, fmt.Errorf("registry https://main-registry.example.com: connection timeout")
220+
}
221+
222+
openBrowserFunc = mockOpenBrowserSuccess
223+
lastOpenedURL = ""
224+
225+
m := DocsCmd{Driver: "test-driver-1"}.GetModelCustom(
226+
baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg},
227+
false,
228+
mockOpenBrowserSuccess,
229+
testFallbackUrls,
230+
)
231+
232+
out := suite.runCmdErr(m)
233+
suite.Contains(out, "connection timeout")
234+
suite.Equal("", lastOpenedURL, "browser should not be opened on error")
235+
}

cmd/dbc/info.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package main
1616

1717
import (
1818
"encoding/json"
19+
"fmt"
1920
"strings"
2021

2122
tea "github.com/charmbracelet/bubbletea"
@@ -45,20 +46,26 @@ func (c InfoCmd) GetModel() tea.Model {
4546
type infoModel struct {
4647
baseModel
4748

48-
driver string
49-
jsonOutput bool
50-
drv dbc.Driver
49+
driver string
50+
jsonOutput bool
51+
drv dbc.Driver
52+
registryErrors error // Store registry errors for better error messages
5153
}
5254

5355
func (m infoModel) Init() tea.Cmd {
5456
return func() tea.Msg {
55-
drivers, err := m.getDriverRegistry()
56-
if err != nil {
57-
return err
57+
drivers, registryErr := m.getDriverRegistry()
58+
// If we have no drivers and there's an error, fail immediately
59+
if len(drivers) == 0 && registryErr != nil {
60+
return fmt.Errorf("error getting driver list: %w", registryErr)
5861
}
5962

6063
drv, err := findDriver(m.driver, drivers)
6164
if err != nil {
65+
// If we have registry errors, enhance the error message
66+
if registryErr != nil {
67+
return fmt.Errorf("%w\n\nNote: Some driver registries were unavailable:\n%s", err, registryErr.Error())
68+
}
6269
return err
6370
}
6471

cmd/dbc/info_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
package main
1616

17+
import (
18+
"fmt"
19+
20+
"github.com/columnar-tech/dbc"
21+
)
22+
1723
func (suite *SubcommandTestSuite) TestInfo() {
1824
m := InfoCmd{Driver: "test-driver-1"}.
1925
GetModelCustom(baseModel{getDriverRegistry: getTestDriverRegistry, downloadPkg: downloadTestPkg})
@@ -34,3 +40,57 @@ func (suite *SubcommandTestSuite) TestInfo_DriverNotFound() {
3440

3541
suite.validateOutput("\r ", "\nError: driver `non-existent-driver` not found in driver registry index", out)
3642
}
43+
44+
func (suite *SubcommandTestSuite) TestInfoPartialRegistryFailure() {
45+
// Test that info command handles partial registry failure gracefully
46+
// (one registry succeeds, another fails - returns both drivers and error)
47+
partialFailingRegistry := func() ([]dbc.Driver, error) {
48+
// Get drivers from the test registry (simulating one successful registry)
49+
drivers, _ := getTestDriverRegistry()
50+
// But also return an error (simulating another registry that failed)
51+
return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: DNS error")
52+
}
53+
54+
// Should succeed if the requested driver is found in the available drivers
55+
m := InfoCmd{Driver: "test-driver-1"}.
56+
GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg})
57+
58+
out := suite.runCmd(m)
59+
// Should display info successfully without printing the registry error
60+
suite.Contains(out, "Driver: test-driver-1")
61+
suite.Contains(out, "Version: 1.1.0")
62+
}
63+
64+
func (suite *SubcommandTestSuite) TestInfoPartialRegistryFailureDriverNotFound() {
65+
// Test that info command shows registry errors when the requested driver is not found
66+
partialFailingRegistry := func() ([]dbc.Driver, error) {
67+
// Get drivers from the test registry (simulating one successful registry)
68+
drivers, _ := getTestDriverRegistry()
69+
// But also return an error (simulating another registry that failed)
70+
return drivers, fmt.Errorf("registry https://secondary-registry.example.com: failed to fetch driver registry: DNS error")
71+
}
72+
73+
// Should fail with enhanced error message if the requested driver is not found
74+
m := InfoCmd{Driver: "nonexistent-driver"}.
75+
GetModelCustom(baseModel{getDriverRegistry: partialFailingRegistry, downloadPkg: downloadTestPkg})
76+
77+
out := suite.runCmdErr(m)
78+
// Should show the driver not found error AND the registry error
79+
suite.Contains(out, "driver `nonexistent-driver` not found")
80+
suite.Contains(out, "Note: Some driver registries were unavailable")
81+
suite.Contains(out, "failed to fetch driver registry")
82+
suite.Contains(out, "DNS error")
83+
}
84+
85+
func (suite *SubcommandTestSuite) TestInfoCompleteRegistryFailure() {
86+
// Test that info command handles complete registry failure (no drivers returned)
87+
completeFailingRegistry := func() ([]dbc.Driver, error) {
88+
return nil, fmt.Errorf("registry https://primary-registry.example.com: network unreachable")
89+
}
90+
91+
m := InfoCmd{Driver: "test-driver-1"}.
92+
GetModelCustom(baseModel{getDriverRegistry: completeFailingRegistry, downloadPkg: downloadTestPkg})
93+
94+
out := suite.runCmdErr(m)
95+
suite.Contains(out, "network unreachable")
96+
}

0 commit comments

Comments
 (0)