9ccb304d2e
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
139 lines
4.2 KiB
Go
139 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/vasyakrg/dns-autoresolver/internal/diff"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/model"
|
|
"github.com/vasyakrg/dns-autoresolver/internal/service"
|
|
)
|
|
|
|
type mockCheckApplier struct {
|
|
lastReq service.ApplyRequest
|
|
zoneRecords []model.Record
|
|
zoneErr error
|
|
}
|
|
|
|
func (m *mockCheckApplier) Check(context.Context, uuid.UUID, uuid.UUID) (diff.Changeset, error) {
|
|
d := model.Record{Type: model.A, Name: "a.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}
|
|
return diff.Changeset{Diffs: []diff.RecordDiff{{Kind: diff.Add, Type: d.Type, Name: d.Name, Desired: &d}}}, nil
|
|
}
|
|
func (m *mockCheckApplier) Apply(_ context.Context, _, _ uuid.UUID, req service.ApplyRequest) (diff.Changeset, error) {
|
|
m.lastReq = req
|
|
return diff.Changeset{}, nil
|
|
}
|
|
func (m *mockCheckApplier) ZoneRecords(context.Context, uuid.UUID, uuid.UUID) ([]model.Record, error) {
|
|
if m.zoneErr != nil {
|
|
return nil, m.zoneErr
|
|
}
|
|
return m.zoneRecords, nil
|
|
}
|
|
|
|
// newTestAPI wires a fixed authenticated user who owns whatever project id
|
|
// is requested (via alwaysOwnedAuthStore/alwaysValidSessions in
|
|
// middleware_test.go) — these tests exercise check/apply behavior past the
|
|
// RequireAuth/RequireProjectAccess boundary, which is covered separately by
|
|
// middleware_test.go's own tests and the IDOR regression.
|
|
func newTestAPI() (*API, *mockCheckApplier) {
|
|
m := &mockCheckApplier{}
|
|
return &API{Svc: m, Auth: alwaysOwnedAuthStore(), Sessions: alwaysValidSessions(uuid.New())}, m
|
|
}
|
|
|
|
func TestCheckEndpoint(t *testing.T) {
|
|
a, _ := newTestAPI()
|
|
router := NewRouter(a)
|
|
|
|
did := uuid.New().String()
|
|
req := requestWithSessionCookie(http.MethodGet,
|
|
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/check", 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 changesetResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp.Updates) != 1 {
|
|
t.Fatalf("expected 1 update in response, got %+v", resp)
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaultsPruneFalse(t *testing.T) {
|
|
a, m := newTestAPI()
|
|
router := NewRouter(a)
|
|
|
|
did := uuid.New().String()
|
|
body := `{"applyUpdates":true}` // applyPrunes отсутствует → false
|
|
req := requestWithSessionCookie(http.MethodPost,
|
|
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply",
|
|
strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
|
}
|
|
if m.lastReq.ApplyPrunes != false || m.lastReq.ApplyUpdates != true {
|
|
t.Fatalf("apply request mismatch: %+v", m.lastReq)
|
|
}
|
|
}
|
|
|
|
func TestApplyEmptyBodyOK(t *testing.T) {
|
|
a, m := newTestAPI()
|
|
router := NewRouter(a)
|
|
|
|
did := uuid.New().String()
|
|
req := requestWithSessionCookie(http.MethodPost,
|
|
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", w.Code, w.Body.String())
|
|
}
|
|
if m.lastReq.ApplyPrunes != false {
|
|
t.Fatalf("expected ApplyPrunes=false for empty body, got %+v", m.lastReq)
|
|
}
|
|
}
|
|
|
|
func TestApplyMalformedBody(t *testing.T) {
|
|
a, _ := newTestAPI()
|
|
router := NewRouter(a)
|
|
|
|
did := uuid.New().String()
|
|
body := `{"applyUpdates":`
|
|
req := requestWithSessionCookie(http.MethodPost,
|
|
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/"+did+"/apply",
|
|
strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for malformed body, got %d body %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestApplyBadUUID(t *testing.T) {
|
|
a, _ := newTestAPI()
|
|
router := NewRouter(a)
|
|
req := requestWithSessionCookie(http.MethodPost,
|
|
"/api/v1/projects/00000000-0000-0000-0000-000000000002/domains/not-a-uuid/apply",
|
|
bytes.NewReader([]byte(`{}`)))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for bad uuid, got %d", w.Code)
|
|
}
|
|
}
|