Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3a217cbe |
@@ -0,0 +1,106 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListCommunityPrefixesByIDAndLabel(t *testing.T) {
|
||||
srv, err := New(Options{
|
||||
InsecureDev: true,
|
||||
SeedDemo: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
||||
mustSetTestAPIKeys(t, srv, "vwkey|"+tenant+"|viewer")
|
||||
|
||||
ts := httptest.NewServer(srv.Handler())
|
||||
defer ts.Close()
|
||||
client := ts.Client()
|
||||
base := ts.URL
|
||||
|
||||
reqList, _ := http.NewRequest(http.MethodGet, base+"/v1/communities?limit=10", nil)
|
||||
reqList.Header.Set("Authorization", "Bearer vwkey")
|
||||
respList, err := client.Do(reqList)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = respList.Body.Close() }()
|
||||
if respList.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(respList.Body)
|
||||
t.Fatalf("communities status %d: %s", respList.StatusCode, b)
|
||||
}
|
||||
var listBody struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
Community string `json:"community"`
|
||||
Title string `json:"title"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(respList.Body).Decode(&listBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(listBody.Items) == 0 {
|
||||
t.Fatal("expected seeded community")
|
||||
}
|
||||
comm := listBody.Items[0]
|
||||
|
||||
assertPrefixesOK := func(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest(http.MethodGet, base+path, nil)
|
||||
req.Header.Set("Authorization", "Bearer vwkey")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("%s status %d: %s", path, resp.StatusCode, b)
|
||||
}
|
||||
var body struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
Prefixes []string `json:"prefixes"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Items == nil {
|
||||
t.Fatalf("%s: expected items array (got nil)", path)
|
||||
}
|
||||
if body.Prefixes == nil {
|
||||
t.Fatalf("%s: expected prefixes array (got nil)", path)
|
||||
}
|
||||
}
|
||||
|
||||
// UUID id
|
||||
assertPrefixesOK(t, "/v1/communities/"+comm.ID+"/prefixes?limit=100")
|
||||
// Community string (legacy / autocomplete label without title)
|
||||
assertPrefixesOK(t, "/v1/communities/"+url.PathEscape(comm.Community)+"/prefixes?limit=100")
|
||||
if comm.Title != "" {
|
||||
// Full Base UI {value,label} display string
|
||||
label := comm.Community + " · " + comm.Title
|
||||
assertPrefixesOK(t, "/v1/communities/"+url.PathEscape(label)+"/prefixes?limit=100")
|
||||
}
|
||||
|
||||
req404, _ := http.NewRequest(http.MethodGet, base+"/v1/communities/missing-community/prefixes", nil)
|
||||
req404.Header.Set("Authorization", "Bearer vwkey")
|
||||
resp404, err := client.Do(req404)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = resp404.Body.Close() }()
|
||||
if resp404.StatusCode != http.StatusNotFound {
|
||||
b, _ := io.ReadAll(resp404.Body)
|
||||
t.Fatalf("expected 404, got %d: %s", resp404.StatusCode, b)
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
||||
|
||||
m.HandleFunc("GET /communities", s.handleListComm)
|
||||
m.HandleFunc("POST /communities", s.handlePostComm)
|
||||
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
||||
m.HandleFunc("GET /communities/{id}/prefixes", s.handleListCommPrefixes)
|
||||
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
||||
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
||||
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
||||
|
||||
@@ -232,6 +232,9 @@ func writePostgresStoreErr(w http.ResponseWriter, err error) bool {
|
||||
case "23505":
|
||||
writeProblem(w, http.StatusConflict, "Conflict", "resource already exists")
|
||||
return true
|
||||
case "22P02":
|
||||
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid id format")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1237,12 +1237,32 @@ func (p *Postgres) ListCommunities(tenantID string) ([]*store.Community, error)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) {
|
||||
func (p *Postgres) GetCommunity(tenantID, idOrKey string) (*store.Community, error) {
|
||||
ctx := context.Background()
|
||||
key := strings.TrimSpace(idOrKey)
|
||||
if key == "" {
|
||||
return nil, store.ErrNotFound
|
||||
}
|
||||
var c store.Community
|
||||
c.TenantID = tenantID
|
||||
err := p.pool.QueryRow(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, id, tenantID).Scan(
|
||||
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
||||
// Prefer UUID id; fall back to community / title so clients that store the
|
||||
// autocomplete label (Base UI {value,label} → label) still resolve.
|
||||
var err error
|
||||
if _, perr := uuid.Parse(key); perr == nil {
|
||||
err = p.pool.QueryRow(ctx, `SELECT id::text, community, title, value_json::text FROM bgp_community WHERE id=$1 AND tenant_id=$2`, key, tenantID).Scan(
|
||||
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
||||
} else {
|
||||
err = p.pool.QueryRow(ctx, `
|
||||
SELECT id::text, community, title, value_json::text FROM bgp_community
|
||||
WHERE tenant_id=$1 AND (
|
||||
community = $2
|
||||
OR title = $2
|
||||
OR (NULLIF(trim(title), '') IS NOT NULL AND (community || ' · ' || title) = $2)
|
||||
)
|
||||
ORDER BY community
|
||||
LIMIT 1`, tenantID, key).Scan(
|
||||
&c.ID, &c.Community, &c.Title, &c.ValueJSON)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, store.ErrNotFound
|
||||
@@ -1253,9 +1273,11 @@ func (p *Postgres) GetCommunity(tenantID, id string) (*store.Community, error) {
|
||||
}
|
||||
|
||||
func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]store.PrefixRow, string, bool, error) {
|
||||
if _, err := p.GetCommunity(tenantID, communityID); err != nil {
|
||||
comm, err := p.GetCommunity(tenantID, communityID)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
resolvedID := comm.ID
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
@@ -1271,7 +1293,6 @@ func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, l
|
||||
ctx := context.Background()
|
||||
useSnap := prefixSnapshotTableExists(ctx, p.pool)
|
||||
var rows pgx.Rows
|
||||
var err error
|
||||
if useSnap {
|
||||
rows, err = p.pool.Query(ctx, `
|
||||
WITH latest AS (
|
||||
@@ -1293,7 +1314,7 @@ func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, l
|
||||
)
|
||||
SELECT prefix, source FROM combined
|
||||
ORDER BY prefix
|
||||
LIMIT $3 OFFSET $4`, tenantID, communityID, limit+1, off)
|
||||
LIMIT $3 OFFSET $4`, tenantID, resolvedID, limit+1, off)
|
||||
} else {
|
||||
rows, err = p.pool.Query(ctx, `
|
||||
WITH latest AS (
|
||||
@@ -1307,20 +1328,19 @@ func (p *Postgres) ListCommunityPrefixes(tenantID, communityID, cursor string, l
|
||||
JOIN latest l ON l.id = rmp.revision_id
|
||||
WHERE rmp.community_id = $2::uuid
|
||||
ORDER BY 1
|
||||
LIMIT $3 OFFSET $4`, tenantID, communityID, limit+1, off)
|
||||
LIMIT $3 OFFSET $4`, tenantID, resolvedID, limit+1, off)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var all []store.PrefixRow
|
||||
comm := communityID
|
||||
for rows.Next() {
|
||||
var pr store.PrefixRow
|
||||
if err := rows.Scan(&pr.Prefix, &pr.Source); err != nil {
|
||||
continue
|
||||
}
|
||||
pr.CommunityID = &comm
|
||||
pr.CommunityID = &resolvedID
|
||||
all = append(all, pr)
|
||||
}
|
||||
more := len(all) > limit
|
||||
|
||||
@@ -585,20 +585,36 @@ func (m *Memory) ListCommunities(tenantID string) ([]*Community, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetCommunity(tenantID, id string) (*Community, error) {
|
||||
func (m *Memory) GetCommunity(tenantID, idOrKey string) (*Community, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.communities[id]
|
||||
if !ok || c.TenantID != tenantID {
|
||||
key := strings.TrimSpace(idOrKey)
|
||||
if key == "" {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return c, nil
|
||||
if c, ok := m.communities[key]; ok && c.TenantID == tenantID {
|
||||
return c, nil
|
||||
}
|
||||
for _, c := range m.communities {
|
||||
if c.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if c.Community == key || c.Title == key {
|
||||
return c, nil
|
||||
}
|
||||
if strings.TrimSpace(c.Title) != "" && c.Community+" · "+c.Title == key {
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, limit int) ([]PrefixRow, string, bool, error) {
|
||||
if _, err := m.GetCommunity(tenantID, communityID); err != nil {
|
||||
commRow, err := m.GetCommunity(tenantID, communityID)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
resolvedID := commRow.ID
|
||||
if limit <= 0 {
|
||||
limit = 500
|
||||
}
|
||||
@@ -626,10 +642,9 @@ func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, lim
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
var all []PrefixRow
|
||||
comm := communityID
|
||||
for _, rev := range latestByModule {
|
||||
for _, pr := range m.revPrefixes[rev.ID] {
|
||||
if pr.CommunityID == nil || *pr.CommunityID != communityID {
|
||||
if pr.CommunityID == nil || *pr.CommunityID != resolvedID {
|
||||
continue
|
||||
}
|
||||
pfx := strings.TrimSpace(pr.Prefix)
|
||||
@@ -640,7 +655,7 @@ func (m *Memory) ListCommunityPrefixes(tenantID, communityID, cursor string, lim
|
||||
continue
|
||||
}
|
||||
seen[pfx] = struct{}{}
|
||||
all = append(all, PrefixRow{Prefix: pfx, CommunityID: &comm, Source: pr.Source})
|
||||
all = append(all, PrefixRow{Prefix: pfx, CommunityID: &resolvedID, Source: pr.Source})
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool { return all[i].Prefix < all[j].Prefix })
|
||||
|
||||
Reference in New Issue
Block a user