fix(httpapi): paginate module nested list endpoints
Пагинация cursor/limit для CDN, AS, domain и IP range list; ответы с next_cursor и has_more по OpenAPI. Добавлен writePaginatedListJSON и тест. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -3,6 +3,8 @@ package httpapi
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"evobgp/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseListLimit(r *http.Request) int {
|
func parseListLimit(r *http.Request) int {
|
||||||
@@ -22,3 +24,15 @@ func strPtrOrNull(s string) any {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writePaginatedListJSON returns a cursor/limit page as OpenAPI list envelopes (items, next_cursor, has_more).
|
||||||
|
func writePaginatedListJSON[T any](w http.ResponseWriter, r *http.Request, all []T, toItem func(T) map[string]any) {
|
||||||
|
page, next, more := store.PaginateOffset(all, r.URL.Query().Get("cursor"), parseListLimit(r))
|
||||||
|
items := make([]map[string]any, 0, len(page))
|
||||||
|
for _, x := range page {
|
||||||
|
items = append(items, toItem(x))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"items": items, "next_cursor": strPtrOrNull(next), "has_more": more,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -193,11 +193,7 @@ func (s *Server) handleListCDNSources(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, len(list))
|
writePaginatedListJSON(w, r, list, cdnSourceJSON)
|
||||||
for _, x := range list {
|
|
||||||
items = append(items, cdnSourceJSON(x))
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items, "next_cursor": nil, "has_more": false})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
||||||
@@ -354,11 +350,7 @@ func (s *Server) handleListAS(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, len(list))
|
writePaginatedListJSON(w, r, list, asEntryJSON)
|
||||||
for _, x := range list {
|
|
||||||
items = append(items, asEntryJSON(x))
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func asEntryJSON(x *store.ASEntry) map[string]any {
|
func asEntryJSON(x *store.ASEntry) map[string]any {
|
||||||
@@ -450,11 +442,7 @@ func (s *Server) handleListDomain(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, len(list))
|
writePaginatedListJSON(w, r, list, domainEntryJSON)
|
||||||
for _, x := range list {
|
|
||||||
items = append(items, domainEntryJSON(x))
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func domainEntryJSON(x *store.DomainEntry) map[string]any {
|
func domainEntryJSON(x *store.DomainEntry) map[string]any {
|
||||||
@@ -531,11 +519,7 @@ func (s *Server) handleListIPRange(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeStoreErr(w, err)
|
writeStoreErr(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, len(list))
|
writePaginatedListJSON(w, r, list, ipRangeJSON)
|
||||||
for _, x := range list {
|
|
||||||
items = append(items, ipRangeJSON(x))
|
|
||||||
}
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipRangeJSON(x *store.IPRangeEntry) map[string]any {
|
func ipRangeJSON(x *store.IPRangeEntry) map[string]any {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNestedModuleListPagination(t *testing.T) {
|
||||||
|
srv, err := New(Options{InsecureDev: true, SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
tenant, _, modIP, _, _ := srv.Store().DemoIDs()
|
||||||
|
srv.apiKeys = parseAPIKeysSpec("edkey|" + tenant + "|editor")
|
||||||
|
|
||||||
|
ts := httptest.NewServer(srv.Handler())
|
||||||
|
defer ts.Close()
|
||||||
|
client := ts.Client()
|
||||||
|
base := ts.URL
|
||||||
|
mid := modIP
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
body := strings.NewReader(fmt.Sprintf(`{"prefix":"10.%d.0.0/24"}`, 200+i))
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, base+"/v1/modules/"+mid+"/ip-range-entries", body)
|
||||||
|
req.Header.Set("Authorization", "Bearer edkey")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
t.Fatalf("create entry %d: status %d", i, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, base+"/v1/modules/"+mid+"/ip-range-entries?limit=2", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer edkey")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
t.Fatalf("list status %d: %s", resp.StatusCode, b)
|
||||||
|
}
|
||||||
|
var page1 struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
NextCursor *string `json:"next_cursor"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&page1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(page1.Items) != 2 {
|
||||||
|
t.Fatalf("page1 items: got %d want 2", len(page1.Items))
|
||||||
|
}
|
||||||
|
if !page1.HasMore || page1.NextCursor == nil || *page1.NextCursor == "" {
|
||||||
|
t.Fatalf("page1: has_more=%v next_cursor=%v", page1.HasMore, page1.NextCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
req2, _ := http.NewRequest(http.MethodGet, base+"/v1/modules/"+mid+"/ip-range-entries?limit=2&cursor="+*page1.NextCursor, nil)
|
||||||
|
req2.Header.Set("Authorization", "Bearer edkey")
|
||||||
|
resp2, err := client.Do(req2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
var page2 struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
HasMore bool `json:"has_more"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp2.Body).Decode(&page2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(page1.Items)+len(page2.Items) < 3 {
|
||||||
|
t.Fatalf("expected at least 3 entries across pages, got %d+%d", len(page1.Items), len(page2.Items))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user