fix(cli): normalize remote route not found errors
- commit
- 6507981d664c4c8152d60152919bf182af4f814c
- author
- brice · · 2026-05-22 07:07 UTC
- parent
- e8ee147
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +53 -0
M internal/adapters/in/cli/remote/client.go +4 -0
| @@ -504,6 +504,10 @@ func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Rout | |||
| 504 | 504 | ||
| 505 | 505 | var route domain.Route | |
| 506 | 506 | if err := parseResponse(resp, &route); err != nil { | |
| 507 | + | var httpErr *HTTPError | |
| 508 | + | if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound { | |
| 509 | + | return nil, domain.ErrRouteNotFound | |
| 510 | + | } | |
| 507 | 511 | return nil, err | |
| 508 | 512 | } | |
| 509 | 513 | ||
| @@ -504,6 +504,10 @@ func (c *Client) GetRoute(ctx context.Context, routeDomain string) (*domain.Rout | |||||
| 504 | 504 | ||||
| 505 | var route domain.Route | 505 | var route domain.Route | ||
| 506 | if err := parseResponse(resp, &route); err != nil { | 506 | if err := parseResponse(resp, &route); err != nil { | ||
| 507 | + | var httpErr *HTTPError | |||
| 508 | + | if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound { | |||
| 509 | + | return nil, domain.ErrRouteNotFound | |||
| 510 | + | } | |||
| 507 | return nil, err | 511 | return nil, err | ||
| 508 | } | 512 | } | ||
| 509 | 513 | ||||
M internal/adapters/in/cli/remote/client_test.go +17 -0
| @@ -13,6 +13,7 @@ import ( | |||
| 13 | 13 | "testing" | |
| 14 | 14 | "time" | |
| 15 | 15 | ||
| 16 | + | "github.com/bnema/gordon/internal/domain" | |
| 16 | 17 | "github.com/stretchr/testify/assert" | |
| 17 | 18 | "github.com/stretchr/testify/require" | |
| 18 | 19 | ||
| @@ -200,6 +201,22 @@ func TestClientFindAttachmentTargetsByImage(t *testing.T) { | |||
| 200 | 201 | assert.Equal(t, []string{"app.example.com", "workers"}, targets) | |
| 201 | 202 | } | |
| 202 | 203 | ||
| 204 | + | func TestClientGetRoute_Maps404ToDomainErrRouteNotFound(t *testing.T) { | |
| 205 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 206 | + | require.Equal(t, "/admin/routes/missing.example.test", r.URL.Path) | |
| 207 | + | require.Equal(t, http.MethodGet, r.Method) | |
| 208 | + | w.WriteHeader(http.StatusNotFound) | |
| 209 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |
| 210 | + | })) | |
| 211 | + | defer srv.Close() | |
| 212 | + | ||
| 213 | + | client := NewClient(srv.URL) | |
| 214 | + | route, err := client.GetRoute(context.Background(), "missing.example.test") | |
| 215 | + | require.Nil(t, route) | |
| 216 | + | require.Error(t, err) | |
| 217 | + | assert.ErrorIs(t, err, domain.ErrRouteNotFound) | |
| 218 | + | } | |
| 219 | + | ||
| 203 | 220 | func TestClientFindAttachmentTargetsByImage_WithSlashContainingImage(t *testing.T) { | |
| 204 | 221 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 205 | 222 | require.Equal(t, "/admin/attachments/by-image/registry/org/image:tag", r.URL.Path) | |
| @@ -13,6 +13,7 @@ import ( | |||||
| 13 | "testing" | 13 | "testing" | ||
| 14 | "time" | 14 | "time" | ||
| 15 | 15 | ||||
| 16 | + | "github.com/bnema/gordon/internal/domain" | |||
| 16 | "github.com/stretchr/testify/assert" | 17 | "github.com/stretchr/testify/assert" | ||
| 17 | "github.com/stretchr/testify/require" | 18 | "github.com/stretchr/testify/require" | ||
| 18 | 19 | ||||
| @@ -200,6 +201,22 @@ func TestClientFindAttachmentTargetsByImage(t *testing.T) { | |||||
| 200 | assert.Equal(t, []string{"app.example.com", "workers"}, targets) | 201 | assert.Equal(t, []string{"app.example.com", "workers"}, targets) | ||
| 201 | } | 202 | } | ||
| 202 | 203 | ||||
| 204 | + | func TestClientGetRoute_Maps404ToDomainErrRouteNotFound(t *testing.T) { | |||
| 205 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 206 | + | require.Equal(t, "/admin/routes/missing.example.test", r.URL.Path) | |||
| 207 | + | require.Equal(t, http.MethodGet, r.Method) | |||
| 208 | + | w.WriteHeader(http.StatusNotFound) | |||
| 209 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |||
| 210 | + | })) | |||
| 211 | + | defer srv.Close() | |||
| 212 | + | ||||
| 213 | + | client := NewClient(srv.URL) | |||
| 214 | + | route, err := client.GetRoute(context.Background(), "missing.example.test") | |||
| 215 | + | require.Nil(t, route) | |||
| 216 | + | require.Error(t, err) | |||
| 217 | + | assert.ErrorIs(t, err, domain.ErrRouteNotFound) | |||
| 218 | + | } | |||
| 219 | + | ||||
| 203 | func TestClientFindAttachmentTargetsByImage_WithSlashContainingImage(t *testing.T) { | 220 | func TestClientFindAttachmentTargetsByImage_WithSlashContainingImage(t *testing.T) { | ||
| 204 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 221 | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 205 | require.Equal(t, "/admin/attachments/by-image/registry/org/image:tag", r.URL.Path) | 222 | require.Equal(t, "/admin/attachments/by-image/registry/org/image:tag", r.URL.Path) | ||
M internal/adapters/in/cli/routes_test.go +32 -0
| @@ -6,6 +6,8 @@ import ( | |||
| 6 | 6 | "encoding/json" | |
| 7 | 7 | "errors" | |
| 8 | 8 | "io" | |
| 9 | + | "net/http" | |
| 10 | + | "net/http/httptest" | |
| 9 | 11 | "os" | |
| 10 | 12 | "path/filepath" | |
| 11 | 13 | "strconv" | |
| @@ -948,6 +950,36 @@ func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerE | |||
| 948 | 950 | assert.Equal(t, "gordon-gordon-app__example__test-postgres-var-lib-postgresql", diag.Volumes[0].Name) | |
| 949 | 951 | } | |
| 950 | 952 | ||
| 953 | + | func TestBuildRouteDiagnosis_RemoteMissingRouteDoesNotWarnOnExpectedNotFound(t *testing.T) { | |
| 954 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 955 | + | w.Header().Set("Content-Type", "application/json") | |
| 956 | + | switch r.URL.Path { | |
| 957 | + | case "/admin/routes/missing.example.test": | |
| 958 | + | w.WriteHeader(http.StatusNotFound) | |
| 959 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |
| 960 | + | case "/admin/routes": | |
| 961 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |
| 962 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |
| 963 | + | case "/admin/health": | |
| 964 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |
| 965 | + | case "/admin/volumes": | |
| 966 | + | _, _ = w.Write([]byte(`[]`)) | |
| 967 | + | case "/admin/attachments/orphans": | |
| 968 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |
| 969 | + | default: | |
| 970 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |
| 971 | + | } | |
| 972 | + | })) | |
| 973 | + | defer srv.Close() | |
| 974 | + | ||
| 975 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |
| 976 | + | diag, err := buildRouteDiagnosis(context.Background(), cp, "missing.example.test") | |
| 977 | + | ||
| 978 | + | require.NoError(t, err) | |
| 979 | + | assert.False(t, diag.Configured) | |
| 980 | + | assert.Empty(t, diag.Warnings) | |
| 981 | + | } | |
| 982 | + | ||
| 951 | 983 | func TestWriteRouteDiagnosisText_ShowsActiveAttachmentsSeparatelyFromOrphans(t *testing.T) { | |
| 952 | 984 | var out bytes.Buffer | |
| 953 | 985 | diag := &routeDiagnosis{ | |
| @@ -6,6 +6,8 @@ import ( | |||||
| 6 | "encoding/json" | 6 | "encoding/json" | ||
| 7 | "errors" | 7 | "errors" | ||
| 8 | "io" | 8 | "io" | ||
| 9 | + | "net/http" | |||
| 10 | + | "net/http/httptest" | |||
| 9 | "os" | 11 | "os" | ||
| 10 | "path/filepath" | 12 | "path/filepath" | ||
| 11 | "strconv" | 13 | "strconv" | ||
| @@ -948,6 +950,36 @@ func TestBuildRouteDiagnosis_IncludesAttachmentVolumesWithDoubleUnderscoreOwnerE | |||||
| 948 | assert.Equal(t, "gordon-gordon-app__example__test-postgres-var-lib-postgresql", diag.Volumes[0].Name) | 950 | assert.Equal(t, "gordon-gordon-app__example__test-postgres-var-lib-postgresql", diag.Volumes[0].Name) | ||
| 949 | } | 951 | } | ||
| 950 | 952 | ||||
| 953 | + | func TestBuildRouteDiagnosis_RemoteMissingRouteDoesNotWarnOnExpectedNotFound(t *testing.T) { | |||
| 954 | + | srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |||
| 955 | + | w.Header().Set("Content-Type", "application/json") | |||
| 956 | + | switch r.URL.Path { | |||
| 957 | + | case "/admin/routes/missing.example.test": | |||
| 958 | + | w.WriteHeader(http.StatusNotFound) | |||
| 959 | + | _, _ = w.Write([]byte(`{"error":"route not found"}`)) | |||
| 960 | + | case "/admin/routes": | |||
| 961 | + | require.Equal(t, "detailed=true", r.URL.RawQuery) | |||
| 962 | + | _, _ = w.Write([]byte(`{"routes":[]}`)) | |||
| 963 | + | case "/admin/health": | |||
| 964 | + | _, _ = w.Write([]byte(`{"health":{}}`)) | |||
| 965 | + | case "/admin/volumes": | |||
| 966 | + | _, _ = w.Write([]byte(`[]`)) | |||
| 967 | + | case "/admin/attachments/orphans": | |||
| 968 | + | _, _ = w.Write([]byte(`{"attachments":[]}`)) | |||
| 969 | + | default: | |||
| 970 | + | t.Fatalf("unexpected path: %s", r.URL.Path) | |||
| 971 | + | } | |||
| 972 | + | })) | |||
| 973 | + | defer srv.Close() | |||
| 974 | + | ||||
| 975 | + | cp := NewRemoteControlPlane(remote.NewClient(srv.URL)) | |||
| 976 | + | diag, err := buildRouteDiagnosis(context.Background(), cp, "missing.example.test") | |||
| 977 | + | ||||
| 978 | + | require.NoError(t, err) | |||
| 979 | + | assert.False(t, diag.Configured) | |||
| 980 | + | assert.Empty(t, diag.Warnings) | |||
| 981 | + | } | |||
| 982 | + | ||||
| 951 | func TestWriteRouteDiagnosisText_ShowsActiveAttachmentsSeparatelyFromOrphans(t *testing.T) { | 983 | func TestWriteRouteDiagnosisText_ShowsActiveAttachmentsSeparatelyFromOrphans(t *testing.T) { | ||
| 952 | var out bytes.Buffer | 984 | var out bytes.Buffer | ||
| 953 | diag := &routeDiagnosis{ | 985 | diag := &routeDiagnosis{ | ||