//go:build e2e package agentnetwork import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/e2e/harness" "github.com/netbirdio/netbird/shared/management/http/api" ) // harnessStartFresh boots a dedicated combined server with its own fresh // account and registers its teardown on t. func harnessStartFresh(ctx context.Context, t *testing.T) (*harness.Combined, error) { t.Helper() fresh, err := harness.StartCombined(ctx) if err != nil { return nil, err } t.Cleanup(func() { _ = fresh.Terminate(context.Background()) }) if _, err := fresh.Bootstrap(ctx); err != nil { return nil, err } return fresh, nil } // TestSettingsBootstrapViaPut covers the settings-first bootstrap path on an // account that has never been bootstrapped: the GET reads as the defaults // with an empty cluster/subdomain/endpoint, a PUT without a cluster has // nothing to pin and fails, and a PUT carrying a cluster creates the row and // pins it immutably. The shared srv cannot provide that starting state (any // provider-creating test bootstraps it, and test order is deliberately not // relied on), so this boots a dedicated combined server — the image is // already built and cached by TestMain's StartCombined, so the extra cost is // one container start. func TestSettingsBootstrapViaPut(t *testing.T) { ctx := context.Background() fresh, err := harnessStartFresh(ctx, t) require.NoError(t, err, "start dedicated combined server") // Before agent-network bootstrap the settings read as the defaults, not // as an error and not as a null body. before, err := fresh.GetSettings(ctx) require.NoError(t, err, "get settings on a fresh account must succeed") assert.Empty(t, before.Cluster, "cluster must be empty before bootstrap") assert.Empty(t, before.Subdomain, "subdomain must be empty before bootstrap") assert.Empty(t, before.Endpoint, "endpoint must be empty before bootstrap, not a bare dot") assert.True(t, before.EnableLogCollection, "defaults must show log collection on, matching bootstrap") assert.False(t, before.EnablePromptCollection, "defaults must show prompt collection off") // A PUT without a cluster has nothing to pin the account to. _, err = fresh.UpdateSettings(ctx, api.AgentNetworkSettingsRequest{ EnableLogCollection: true, }) requireClientError(t, err) // A PUT carrying a cluster bootstraps the account and applies the // mutable fields from the same request. Every toggle is set away from // its bootstrap default so each assertion can actually fail. const cluster = "e2e.bootstrap.netbird.selfhosted" bootstrapped, err := fresh.UpdateSettings(ctx, api.AgentNetworkSettingsRequest{ Cluster: ptr(cluster), EnableLogCollection: false, EnablePromptCollection: true, RedactPii: true, }) require.NoError(t, err, "bootstrap settings via PUT must succeed") assert.Equal(t, cluster, bootstrapped.Cluster, "cluster must be pinned from the request") require.NotEmpty(t, bootstrapped.Subdomain, "subdomain must be assigned at bootstrap") assert.Equal(t, bootstrapped.Subdomain+"."+cluster, bootstrapped.Endpoint, "endpoint must combine subdomain and cluster") assert.False(t, bootstrapped.EnableLogCollection, "log collection from the bootstrap request must override the default") assert.True(t, bootstrapped.EnablePromptCollection, "prompt collection from the bootstrap request must apply") assert.True(t, bootstrapped.RedactPii, "redact toggle from the bootstrap request must apply") // The row is persisted: an independent read agrees on every field. after, err := fresh.GetSettings(ctx) require.NoError(t, err, "get settings after bootstrap must succeed") assert.Equal(t, bootstrapped.Endpoint, after.Endpoint, "bootstrap must persist across reads") assert.Equal(t, bootstrapped.EnableLogCollection, after.EnableLogCollection, "log collection must persist") assert.Equal(t, bootstrapped.EnablePromptCollection, after.EnablePromptCollection, "prompt collection must persist") assert.Equal(t, bootstrapped.RedactPii, after.RedactPii, "redact toggle must persist") // Once bootstrapped, later updates may omit the cluster entirely. persisted, err := fresh.UpdateSettings(ctx, api.AgentNetworkSettingsRequest{ EnableLogCollection: true, EnablePromptCollection: false, RedactPii: true, }) require.NoError(t, err, "post-bootstrap update without cluster must succeed") assert.Equal(t, cluster, persisted.Cluster, "omitted cluster must keep the pinned value") assert.True(t, persisted.EnableLogCollection, "post-bootstrap toggle must apply") assert.False(t, persisted.EnablePromptCollection, "post-bootstrap toggle must apply") // The cluster is immutable: a different value is rejected rather than // silently ignored, and the rejected update must not disturb anything. _, err = fresh.UpdateSettings(ctx, api.AgentNetworkSettingsRequest{ Cluster: ptr("other.cluster.invalid"), EnableLogCollection: false, }) requireClientError(t, err) final, err := fresh.GetSettings(ctx) require.NoError(t, err, "get settings after the rejected cluster change must succeed") assert.Equal(t, persisted.Cluster, final.Cluster, "rejected update must not change the cluster") assert.Equal(t, persisted.Endpoint, final.Endpoint, "rejected update must not change the endpoint") assert.Equal(t, persisted.EnableLogCollection, final.EnableLogCollection, "rejected update must not apply its toggles") assert.Equal(t, persisted.EnablePromptCollection, final.EnablePromptCollection, "rejected update must not apply its toggles") assert.Equal(t, persisted.RedactPii, final.RedactPii, "rejected update must not apply its toggles") }