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 } 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 } // 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) } }