fix(api): distinguish domain-not-found (404) from provider failure (502) on zone endpoints

Introduce service.ErrProviderUnavailable, wrapped only around the
provider GetRecords call in ZoneRecords. handleZoneRecords and
handleTemplateFromZone now use errors.Is against it to tell a real
provider outage (502) apart from local resolution failures such as an
unknown domain (404), instead of collapsing every ZoneRecords error
into a blanket 502. Also fixes handleTemplateFromZone's GetDomain
error branch to return 404 "domain not found" instead of 500, for
consistency with handleSetDomainTemplate/handleDomainHistory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
This commit is contained in:
2026-07-05 12:14:46 +07:00
parent 9ccb304d2e
commit 5662334799
3 changed files with 53 additions and 7 deletions
+25 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/vasyakrg/dns-autoresolver/internal/diff"
"github.com/vasyakrg/dns-autoresolver/internal/model"
"github.com/vasyakrg/dns-autoresolver/internal/provider"
"github.com/vasyakrg/dns-autoresolver/internal/service"
"github.com/vasyakrg/dns-autoresolver/internal/store"
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
)
@@ -688,13 +690,13 @@ func TestTemplateFromZone_SnapshotsManagedRecordsOnlyAndAttaches(t *testing.T) {
}
// TestZoneRecords_ProviderErrorReturns502 covers the provider-failure path:
// a GetRecords error from the provider must surface as 502 (bad gateway),
// not a generic 500 or a hung response.
// an error wrapping service.ErrProviderUnavailable (i.e. GetRecords itself
// failed) must surface as 502 (bad gateway), not a generic 500 or 404.
func TestZoneRecords_ProviderErrorReturns502(t *testing.T) {
a, ts := newTenantTestAPI()
domID := uuid.New()
ts.domains = []store.Domain{{ID: domID, ZoneName: "example.com", ZoneID: "z1"}}
a.Svc = &mockCheckApplier{zoneErr: errors.New("provider unreachable")}
a.Svc = &mockCheckApplier{zoneErr: fmt.Errorf("%w: boom", service.ErrProviderUnavailable)}
router := NewRouter(a)
req := requestWithSessionCookie(http.MethodGet, "/api/v1/projects/"+testPID+"/domains/"+domID.String()+"/records", nil)
@@ -705,3 +707,23 @@ func TestZoneRecords_ProviderErrorReturns502(t *testing.T) {
t.Fatalf("expected 502, got %d body %s", w.Code, w.Body.String())
}
}
// TestZoneRecords_NotFoundReturns404 covers the domain-resolution-failure
// path: an error that does NOT wrap service.ErrProviderUnavailable (e.g. the
// domain doesn't exist in this project) must surface as 404, not 502 — the
// provider was never even reached.
func TestZoneRecords_NotFoundReturns404(t *testing.T) {
a, ts := newTenantTestAPI()
domID := uuid.New()
ts.domains = []store.Domain{{ID: domID, ZoneName: "example.com", ZoneID: "z1"}}
a.Svc = &mockCheckApplier{zoneErr: errors.New("not found")}
router := NewRouter(a)
req := requestWithSessionCookie(http.MethodGet, "/api/v1/projects/"+testPID+"/domains/"+domID.String()+"/records", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d body %s", w.Code, w.Body.String())
}
}