test(selectel): покрытие id-not-found, токена на мутациях, пагинации и HTTP-ошибок

This commit is contained in:
2026-07-03 12:48:59 +07:00
parent c0f5748817
commit 9b38f081f4
+163
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings"
"testing" "testing"
"github.com/vasyakrg/dns-autoresolver/internal/diff" "github.com/vasyakrg/dns-autoresolver/internal/diff"
@@ -130,3 +131,165 @@ func TestApplyChangesRoutesVerbs(t *testing.T) {
} }
} }
} }
// Global Constraint: id not found for Update -> error, and mutation must not proceed.
func TestApplyChangesUpdateIDNotFoundReturnsErrorAndSkipsMutation(t *testing.T) {
var calls []string
c, srv := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// empty existing rrset set -> nothing resolves to an id
json.NewEncoder(w).Encode(map[string]any{"result": []map[string]any{}, "next_offset": 0})
return
}
calls = append(calls, r.Method+" "+r.URL.Path)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
missing := model.Record{Type: model.A, Name: "missing.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}
add := model.Record{Type: model.A, Name: "new.example.com.", TTL: 300, Values: []string{"2.2.2.2"}}
cs := diff.Changeset{Diffs: []diff.RecordDiff{
{Kind: diff.Update, Type: missing.Type, Name: missing.Name, Desired: &missing},
{Kind: diff.Add, Type: add.Type, Name: add.Name, Desired: &add},
}}
err := c.ApplyChanges(context.Background(), creds(), "z1", cs)
if err == nil {
t.Fatal("expected non-nil error when update rrset id is not found")
}
if len(calls) != 0 {
t.Fatalf("expected no mutating requests to be sent, got %v", calls)
}
}
// Global Constraint: id not found for Delete -> error.
func TestApplyChangesDeleteIDNotFoundReturnsError(t *testing.T) {
c, srv := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
json.NewEncoder(w).Encode(map[string]any{"result": []map[string]any{}, "next_offset": 0})
return
}
t.Fatalf("unexpected mutating call %s %s, delete should have errored before reaching HTTP", r.Method, r.URL.Path)
}))
defer srv.Close()
missing := model.Record{Type: model.A, Name: "missing.example.com.", TTL: 300, Values: []string{"1.1.1.1"}}
cs := diff.Changeset{Diffs: []diff.RecordDiff{
{Kind: diff.Delete, Type: missing.Type, Name: missing.Name, Actual: &missing},
}}
if err := c.ApplyChanges(context.Background(), creds(), "z1", cs); err == nil {
t.Fatal("expected non-nil error when delete rrset id is not found")
}
}
// Global Constraint: X-Auth-Token must be sent on mutating requests (POST/PATCH/DELETE), not only on GET.
func TestApplyChangesSendsTokenOnMutations(t *testing.T) {
var tokens []string
c, srv := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
json.NewEncoder(w).Encode(map[string]any{
"result": []map[string]any{
{"id": "up1", "name": "b.example.com.", "type": "A", "ttl": 300,
"records": []map[string]any{{"content": "9.9.9.9"}}},
{"id": "del1", "name": "d.example.com.", "type": "A", "ttl": 300,
"records": []map[string]any{{"content": "4.4.4.4"}}},
},
"next_offset": 0,
})
return
}
tokens = append(tokens, r.Header.Get("X-Auth-Token"))
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
add := model.Record{Type: model.A, Name: "c.example.com.", TTL: 300, Values: []string{"3.3.3.3"}}
updDesired := model.Record{Type: model.A, Name: "b.example.com.", TTL: 300, Values: []string{"2.2.2.2"}}
delActual := model.Record{Type: model.A, Name: "d.example.com.", TTL: 300, Values: []string{"4.4.4.4"}}
cs := diff.Changeset{Diffs: []diff.RecordDiff{
{Kind: diff.Add, Type: add.Type, Name: add.Name, Desired: &add},
{Kind: diff.Update, Type: updDesired.Type, Name: updDesired.Name, Desired: &updDesired},
{Kind: diff.Delete, Type: delActual.Type, Name: delActual.Name, Actual: &delActual},
}}
if err := c.ApplyChanges(context.Background(), creds(), "z1", cs); err != nil {
t.Fatal(err)
}
if len(tokens) != 3 {
t.Fatalf("expected 3 mutating requests (POST/PATCH/DELETE), got %d", len(tokens))
}
for _, tok := range tokens {
if tok != "secret-token" {
t.Fatalf("expected X-Auth-Token %q on mutation, got %q", "secret-token", tok)
}
}
}
// Global Constraint: multi-page pagination must accumulate records across pages without an infinite loop.
func TestListZonesPaginatesAcrossMultiplePages(t *testing.T) {
var offsets []string
c, srv := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
offset := r.URL.Query().Get("offset")
offsets = append(offsets, offset)
if len(offsets) > 2 {
t.Fatalf("too many requests, possible infinite pagination loop: %v", offsets)
}
switch offset {
case "0":
json.NewEncoder(w).Encode(map[string]any{
"result": []map[string]any{{"id": "z1", "name": "first.example.com."}},
"next_offset": 1000,
})
case "1000":
json.NewEncoder(w).Encode(map[string]any{
"result": []map[string]any{{"id": "z2", "name": "second.example.com."}},
"next_offset": 0,
})
default:
t.Fatalf("unexpected offset %q", offset)
}
}))
defer srv.Close()
zs, err := c.ListZones(context.Background(), creds())
if err != nil {
t.Fatal(err)
}
if len(offsets) != 2 {
t.Fatalf("expected exactly 2 page requests, got %d: %v", len(offsets), offsets)
}
if len(zs) != 2 || zs[0].ID != "z1" || zs[1].ID != "z2" {
t.Fatalf("expected accumulated zones from both pages, got %+v", zs)
}
}
// Global Constraint: HTTP errors (status >= 300) must surface a non-nil error whose text
// includes the method/path/status (or response body) for diagnosability.
func TestListZonesHTTPErrorIncludesMethodPathStatus(t *testing.T) {
c, srv := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("zone not found"))
}))
defer srv.Close()
_, err := c.ListZones(context.Background(), creds())
if err == nil {
t.Fatal("expected non-nil error on non-2xx response")
}
msg := err.Error()
if !strings.Contains(msg, http.MethodGet) {
t.Fatalf("error should mention HTTP method %q, got %q", http.MethodGet, msg)
}
if !strings.Contains(msg, "404") {
t.Fatalf("error should mention status code 404, got %q", msg)
}
if !strings.Contains(msg, "/zones") {
t.Fatalf("error should mention request path, got %q", msg)
}
if !strings.Contains(msg, "zone not found") {
t.Fatalf("error should include response body, got %q", msg)
}
}