feat(api): read zone records without template + snapshot-to-template
LoadDomain requires a template, so a zone without one could never be
viewed or snapshotted. Adds a template-free path: store.LoadZone /
service.ZoneRef / DomainService.ZoneRecords read a zone's live records
straight from the provider (no diff, no template). GET
/domains/{did}/records exposes read-only viewing; POST
/domains/{did}/template-from-zone snapshots only managed record types
(NS/SOA excluded) into a new template and auto-attaches it to the domain.
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:
@@ -98,6 +98,15 @@ func (m *mockTenantStore) ListDomains(context.Context, uuid.UUID) ([]store.Domai
|
||||
return m.domains, nil
|
||||
}
|
||||
|
||||
func (m *mockTenantStore) GetDomain(_ context.Context, id, _ uuid.UUID) (store.Domain, error) {
|
||||
for _, d := range m.domains {
|
||||
if d.ID == id {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
return store.Domain{}, errors.New("domain not found")
|
||||
}
|
||||
|
||||
func (m *mockTenantStore) DeleteDomain(context.Context, uuid.UUID, uuid.UUID) error { return nil }
|
||||
|
||||
func (m *mockTenantStore) SetDomainTemplate(_ context.Context, domainID, projectID uuid.UUID, templateID *uuid.UUID) (store.Domain, error) {
|
||||
@@ -599,3 +608,100 @@ func TestDeleteDomain_BadUUID(t *testing.T) {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// --- zone view / template-from-zone (Task 1: no-template zone snapshot) ---
|
||||
|
||||
// TestZoneRecords_ReturnsProviderRecords covers the read-only zone-viewing
|
||||
// endpoint: it returns whatever the service reads straight from the
|
||||
// provider, with no template involved at all.
|
||||
func TestZoneRecords_ReturnsProviderRecords(t *testing.T) {
|
||||
a, ts := newTenantTestAPI()
|
||||
domID := uuid.New()
|
||||
ts.domains = []store.Domain{{ID: domID, ZoneName: "example.com", ZoneID: "z1"}}
|
||||
a.Svc = &mockCheckApplier{zoneRecords: []model.Record{
|
||||
{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}},
|
||||
}}
|
||||
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.StatusOK {
|
||||
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp []dto.RecordDTO
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp) != 1 || resp[0].Type != "A" {
|
||||
t.Fatalf("unexpected records response: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateFromZone_SnapshotsManagedRecordsOnlyAndAttaches covers the
|
||||
// snapshot-to-template flow: NS/SOA are read-only and must be excluded from
|
||||
// the generated template, and the new template must be auto-attached to the
|
||||
// domain (SetDomainTemplate) so check/apply become immediately available.
|
||||
func TestTemplateFromZone_SnapshotsManagedRecordsOnlyAndAttaches(t *testing.T) {
|
||||
a, ts := newTenantTestAPI()
|
||||
domID := uuid.New()
|
||||
ts.domains = []store.Domain{{ID: domID, ZoneName: "example.com", ZoneID: "z1"}}
|
||||
a.Svc = &mockCheckApplier{zoneRecords: []model.Record{
|
||||
{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}},
|
||||
{Type: model.TXT, Name: "a.example.com.", TTL: 300, Values: []string{"v=spf1 -all"}},
|
||||
{Type: model.NS, Name: "example.com.", TTL: 3600, Values: []string{"ns1.example.com."}},
|
||||
{Type: model.SOA, Name: "example.com.", TTL: 3600, Values: []string{"ns1.example.com. admin.example.com. 1 2 3 4 5"}},
|
||||
}}
|
||||
router := NewRouter(a)
|
||||
|
||||
req := requestWithSessionCookie(http.MethodPost, "/api/v1/projects/"+testPID+"/domains/"+domID.String()+"/template-from-zone", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
if ts.createTemplate == nil {
|
||||
t.Fatal("expected CreateTemplate to be called")
|
||||
}
|
||||
if len(ts.createTemplate.Doc.Records) != 2 {
|
||||
t.Fatalf("expected only the 2 managed records (A+TXT) in the snapshot, got %+v", ts.createTemplate.Doc.Records)
|
||||
}
|
||||
for _, r := range ts.createTemplate.Doc.Records {
|
||||
if r.Type == "NS" || r.Type == "SOA" {
|
||||
t.Fatalf("read-only record type %s leaked into snapshot template", r.Type)
|
||||
}
|
||||
}
|
||||
// SetDomainTemplate must have been called with the newly created template's id.
|
||||
if ts.domains[0].TemplateID == nil || *ts.domains[0].TemplateID != ts.createTemplate.ID {
|
||||
t.Fatalf("expected domain auto-attached to new template %s, got %+v", ts.createTemplate.ID, ts.domains[0].TemplateID)
|
||||
}
|
||||
|
||||
var resp templateResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.ID != ts.createTemplate.ID.String() || len(resp.Records) != 2 {
|
||||
t.Fatalf("unexpected response: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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")}
|
||||
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.StatusBadGateway {
|
||||
t.Fatalf("expected 502, got %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user