fix(selectel): nil-guard в ApplyChanges + тесты пагинации rrset и nil-diff

This commit is contained in:
2026-07-03 12:57:30 +07:00
parent b50972f38d
commit 86338f846a
2 changed files with 87 additions and 0 deletions
+9
View File
@@ -159,10 +159,16 @@ func (c *Client) ApplyChanges(ctx context.Context, creds provider.Credentials, z
} }
switch d.Kind { switch d.Kind {
case diff.Add: case diff.Add:
if d.Desired == nil {
return fmt.Errorf("selectel: add/update diff without Desired record")
}
if err := c.do(ctx, http.MethodPost, base, creds.Secret, toRRSet(*d.Desired), nil); err != nil { if err := c.do(ctx, http.MethodPost, base, creds.Secret, toRRSet(*d.Desired), nil); err != nil {
return err return err
} }
case diff.Update: case diff.Update:
if d.Desired == nil {
return fmt.Errorf("selectel: add/update diff without Desired record")
}
id, ok := idByKey[d.Desired.Key()] id, ok := idByKey[d.Desired.Key()]
if !ok { if !ok {
return fmt.Errorf("cannot update: rrset %s not found in zone", d.Desired.Key()) return fmt.Errorf("cannot update: rrset %s not found in zone", d.Desired.Key())
@@ -171,6 +177,9 @@ func (c *Client) ApplyChanges(ctx context.Context, creds provider.Credentials, z
return err return err
} }
case diff.Delete: case diff.Delete:
if d.Actual == nil {
return fmt.Errorf("selectel: delete diff without Actual record")
}
id, ok := idByKey[d.Actual.Key()] id, ok := idByKey[d.Actual.Key()]
if !ok { if !ok {
return fmt.Errorf("cannot delete: rrset %s not found in zone", d.Actual.Key()) return fmt.Errorf("cannot delete: rrset %s not found in zone", d.Actual.Key())
@@ -266,6 +266,84 @@ func TestListZonesPaginatesAcrossMultiplePages(t *testing.T) {
} }
} }
// Global Constraint: listRRSets (via GetRecords) must paginate across multiple pages,
// accumulating records from every page, and must stop as soon as next_offset is 0 —
// no third request should ever be issued.
func TestGetRecordsPaginatesAcrossMultiplePages(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": "r1", "name": "a.example.com.", "type": "A", "ttl": 300,
"records": []map[string]any{{"content": "1.1.1.1"}}},
},
"next_offset": 1000,
})
case "1000":
json.NewEncoder(w).Encode(map[string]any{
"result": []map[string]any{
{"id": "r2", "name": "b.example.com.", "type": "A", "ttl": 300,
"records": []map[string]any{{"content": "2.2.2.2"}}},
},
"next_offset": 0,
})
default:
t.Fatalf("unexpected offset %q", offset)
}
}))
defer srv.Close()
recs, err := c.GetRecords(context.Background(), creds(), "z1")
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(recs) != 2 {
t.Fatalf("expected accumulated records from both pages, got %+v", recs)
}
names := map[string]bool{recs[0].Name: true, recs[1].Name: true}
if !names["a.example.com."] || !names["b.example.com."] {
t.Fatalf("expected records from both pages, got %+v", recs)
}
}
// Global Constraint: ApplyChanges must not panic on a Changeset with a nil Desired
// record for Add/Update, and must instead return a clear error.
func TestApplyChangesAddWithNilDesiredReturnsErrorNoPanic(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, nil Desired should have errored before reaching HTTP", r.Method, r.URL.Path)
}))
defer srv.Close()
cs := diff.Changeset{Diffs: []diff.RecordDiff{
{Kind: diff.Add, Type: model.A, Name: "nil-desired.example.com.", Desired: nil},
}}
defer func() {
if r := recover(); r != nil {
t.Fatalf("ApplyChanges panicked on nil Desired: %v", r)
}
}()
err := c.ApplyChanges(context.Background(), creds(), "z1", cs)
if err == nil {
t.Fatal("expected non-nil error for Add diff with nil Desired")
}
}
// Global Constraint: HTTP errors (status >= 300) must surface a non-nil error whose text // Global Constraint: HTTP errors (status >= 300) must surface a non-nil error whose text
// includes the method/path/status (or response body) for diagnosability. // includes the method/path/status (or response body) for diagnosability.
func TestListZonesHTTPErrorIncludesMethodPathStatus(t *testing.T) { func TestListZonesHTTPErrorIncludesMethodPathStatus(t *testing.T) {