mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-11 04:26:04 -04:00
## Describe your changes Work on the Terraform provider (terraform-provider-netbird #177–#183) surfaced places where the agent-network API broke its own contracts or deviated from the conventions the rest of the management API follows, forcing client-side workarounds. Settings reads now follow the settings-endpoint convention: GET always answers with a JSON object. Before bootstrap it returns the defaults with an empty cluster/subdomain/endpoint (previously 200 with a JSON `null` body, while the spec said 404). The settings PUT can bootstrap the account by carrying a `cluster` — previously the row could only come into existence through the first provider create, and a settings-first setup was impossible; a differing cluster on a bootstrapped account is rejected instead of silently ignored. PUT remains full-state. The provider PUT schema promised omit-preserves semantics for several operator-editable fields that the handler never delivered (it builds the row from the request, like every other update handler). The schema wording now matches the shipped full-state behavior; only the api_key (secret) and session keys stay preserved by the manager. Identity headers are always present in provider responses so an explicitly cleared value round-trips as an empty string. The Go REST client gains the full agent-network surface (catalog, providers, policies, guardrails, budget rules, settings), including a shim translating the legacy 200+`null` settings body from older servers into an `IsNotFound` error. Note for reviewers: the dashboard special-cased the `null` settings body; it needs a small follow-up for the new defaults response (in progress).
138 lines
7.5 KiB
Go
138 lines
7.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
)
|
|
|
|
// TestSettingsHandler_GetUnbootstrappedReturnsDefaults pins the settings-read
|
|
// convention shared with the account and DNS settings endpoints: settings
|
|
// always read as a JSON object. Before bootstrap that object carries the
|
|
// defaults with an empty cluster/subdomain/endpoint (the "not bootstrapped"
|
|
// signal) and no timestamps — never a 404 and never the legacy null body.
|
|
func TestSettingsHandler_GetUnbootstrappedReturnsDefaults(t *testing.T) {
|
|
f := newAgentNetworkHandlerFixture(t)
|
|
|
|
rec := f.do(t, http.MethodGet, "/agent-network/settings", "")
|
|
require.Equal(t, http.StatusOK, rec.Code,
|
|
"unbootstrapped account must read as 200 with defaults: got %d body=%s", rec.Code, rec.Body.String())
|
|
require.NotEqual(t, "null", trimSpace(rec.Body.String()),
|
|
"the legacy 200+null shape must not come back")
|
|
|
|
var got api.AgentNetworkSettings
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
assert.Empty(t, got.Cluster, "cluster must be empty until bootstrapped")
|
|
assert.Empty(t, got.Subdomain, "subdomain must be empty until bootstrapped")
|
|
assert.Empty(t, got.Endpoint, "endpoint must be empty until bootstrapped, not a bare dot")
|
|
assert.True(t, got.EnableLogCollection, "defaults must show log collection on, matching bootstrap")
|
|
assert.False(t, got.EnablePromptCollection, "defaults must show prompt collection off")
|
|
assert.False(t, got.RedactPii, "defaults must show redaction off")
|
|
require.NotNil(t, got.AccessLogRetentionDays)
|
|
assert.Equal(t, 30, *got.AccessLogRetentionDays, "defaults must show the bootstrap retention")
|
|
assert.Nil(t, got.CreatedAt, "no timestamps before a row exists")
|
|
assert.Nil(t, got.UpdatedAt, "no timestamps before a row exists")
|
|
}
|
|
|
|
// TestSettingsHandler_PutBootstrapsWithCluster covers the settings-first
|
|
// bootstrap path: a PUT carrying a cluster on an unbootstrapped account
|
|
// creates the row (cluster pinned, subdomain assigned) and applies the
|
|
// mutable fields from the same request.
|
|
func TestSettingsHandler_PutBootstrapsWithCluster(t *testing.T) {
|
|
f := newAgentNetworkHandlerFixture(t)
|
|
|
|
rec := f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"cluster": "eu.proxy.netbird.io", "enable_log_collection": true, "enable_prompt_collection": true, "redact_pii": false, "access_log_retention_days": 30}`)
|
|
require.Equal(t, http.StatusOK, rec.Code, "bootstrap PUT must succeed: %s", rec.Body.String())
|
|
|
|
var got api.AgentNetworkSettings
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
assert.Equal(t, "eu.proxy.netbird.io", got.Cluster, "cluster must be pinned from the request")
|
|
assert.NotEmpty(t, got.Subdomain, "subdomain must be assigned at bootstrap")
|
|
assert.Equal(t, got.Subdomain+".eu.proxy.netbird.io", got.Endpoint, "endpoint must combine subdomain and cluster")
|
|
assert.True(t, got.EnableLogCollection, "toggle from the bootstrap request must apply")
|
|
assert.True(t, got.EnablePromptCollection, "toggle from the bootstrap request must apply")
|
|
require.NotNil(t, got.AccessLogRetentionDays)
|
|
assert.Equal(t, 30, *got.AccessLogRetentionDays, "retention from the bootstrap request must apply")
|
|
|
|
// The row is now readable via GET.
|
|
rec = f.do(t, http.MethodGet, "/agent-network/settings", "")
|
|
require.Equal(t, http.StatusOK, rec.Code, "GET after bootstrap must succeed")
|
|
}
|
|
|
|
// TestSettingsHandler_PutWithoutClusterOnUnbootstrapped pins that a PUT
|
|
// without a cluster cannot conjure a settings row out of nothing — there is
|
|
// no cluster to pin — and surfaces as 404 like the GET.
|
|
func TestSettingsHandler_PutWithoutClusterOnUnbootstrapped(t *testing.T) {
|
|
f := newAgentNetworkHandlerFixture(t)
|
|
|
|
rec := f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"enable_log_collection": false, "enable_prompt_collection": false, "redact_pii": false}`)
|
|
assert.Equal(t, http.StatusNotFound, rec.Code,
|
|
"cluster-less PUT on an unbootstrapped account must 404: got %d body=%s", rec.Code, rec.Body.String())
|
|
assert.Contains(t, rec.Body.String(), "cluster",
|
|
"the error must point the caller at the bootstrap paths: %s", rec.Body.String())
|
|
}
|
|
|
|
// TestSettingsHandler_PutReplacesMutableFields pins the update contract shared
|
|
// with the other PUT endpoints: the request replaces every mutable field, so a
|
|
// toggle absent from the JSON lands as its zero value rather than being
|
|
// preserved. Cluster and subdomain survive untouched.
|
|
func TestSettingsHandler_PutReplacesMutableFields(t *testing.T) {
|
|
f := newAgentNetworkHandlerFixture(t)
|
|
|
|
rec := f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"cluster": "eu.proxy.netbird.io", "enable_log_collection": true, "enable_prompt_collection": true, "redact_pii": true, "access_log_retention_days": 14}`)
|
|
require.Equal(t, http.StatusOK, rec.Code, "bootstrap PUT must succeed: %s", rec.Body.String())
|
|
|
|
var before api.AgentNetworkSettings
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &before))
|
|
|
|
rec = f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"enable_log_collection": true, "enable_prompt_collection": false, "redact_pii": false}`)
|
|
require.Equal(t, http.StatusOK, rec.Code, "update PUT must succeed: %s", rec.Body.String())
|
|
|
|
var got api.AgentNetworkSettings
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
assert.True(t, got.EnableLogCollection, "sent toggle must apply")
|
|
assert.False(t, got.EnablePromptCollection, "sent toggle must apply")
|
|
assert.False(t, got.RedactPii, "sent toggle must apply")
|
|
require.NotNil(t, got.AccessLogRetentionDays)
|
|
assert.Equal(t, 0, *got.AccessLogRetentionDays,
|
|
"retention absent from the request must land as the zero value — PUT replaces all mutable fields")
|
|
assert.Equal(t, before.Cluster, got.Cluster, "cluster must survive updates untouched")
|
|
assert.Equal(t, before.Subdomain, got.Subdomain, "subdomain must survive updates untouched")
|
|
}
|
|
|
|
// TestSettingsHandler_PutRejectsClusterChange pins cluster immutability: once
|
|
// assigned, a differing cluster is rejected as a validation error instead of
|
|
// being silently ignored, so callers never observe a value other than the one
|
|
// they sent. Echoing the assigned cluster back stays valid, which lets
|
|
// declarative clients send their full desired state idempotently.
|
|
func TestSettingsHandler_PutRejectsClusterChange(t *testing.T) {
|
|
f := newAgentNetworkHandlerFixture(t)
|
|
|
|
rec := f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"cluster": "eu.proxy.netbird.io", "enable_log_collection": true, "enable_prompt_collection": false, "redact_pii": false}`)
|
|
require.Equal(t, http.StatusOK, rec.Code, "bootstrap PUT must succeed: %s", rec.Body.String())
|
|
|
|
rec = f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"cluster": "us.proxy.netbird.io", "enable_log_collection": true, "enable_prompt_collection": false, "redact_pii": false}`)
|
|
assert.Equal(t, http.StatusUnprocessableEntity, rec.Code,
|
|
"cluster change must be rejected as a validation error: got %d body=%s", rec.Code, rec.Body.String())
|
|
|
|
rec = f.do(t, http.MethodPut, "/agent-network/settings",
|
|
`{"cluster": "eu.proxy.netbird.io", "enable_log_collection": true, "enable_prompt_collection": false, "redact_pii": true}`)
|
|
require.Equal(t, http.StatusOK, rec.Code, "echoing the assigned cluster must stay valid: %s", rec.Body.String())
|
|
|
|
var got api.AgentNetworkSettings
|
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
|
assert.Equal(t, "eu.proxy.netbird.io", got.Cluster, "cluster must be unchanged")
|
|
assert.True(t, got.RedactPii, "toggle sent alongside the echoed cluster must apply")
|
|
}
|