mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 09:53:25 +00:00
445 lines
13 KiB
Go
445 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/telemetry"
|
|
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
// setupTelemetryTest creates a handler with API token auth configured.
|
|
func setupTelemetryTest(t *testing.T, cfg *config.Config) (*SystemSettingsHandler, *config.ConfigPersistence, string) {
|
|
t.Helper()
|
|
persistence := config.NewConfigPersistence(cfg.DataPath)
|
|
|
|
tokenVal := "telemetry-test-token-123.12345678"
|
|
tokenHash := internalauth.HashAPIToken(tokenVal)
|
|
cfg.APITokens = []config.APITokenRecord{
|
|
{ID: "tok1", Hash: tokenHash, Name: "Test Token"},
|
|
}
|
|
|
|
handler := newTestSystemSettingsHandler(cfg, persistence, &mockMonitor{}, func() {}, func() error { return nil })
|
|
return handler, persistence, tokenVal
|
|
}
|
|
|
|
func TestTelemetryUpdate_EnvLockRejects(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: map[string]bool{"PULSE_TELEMETRY": true, "telemetryEnabled": true},
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{"telemetryEnabled": false})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409 Conflict when env-locked, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Verify in-memory config was NOT changed.
|
|
if !cfg.TelemetryEnabled {
|
|
t.Error("TelemetryEnabled should still be true after env-lock rejection")
|
|
}
|
|
}
|
|
|
|
func TestTelemetryUpdate_NullIsIgnored(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
enabled := true
|
|
initial.TelemetryEnabled = &enabled
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Send telemetryEnabled: null via raw JSON.
|
|
body := []byte(`{"telemetryEnabled": null}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// In-memory should remain true (null should not flip it).
|
|
if !cfg.TelemetryEnabled {
|
|
t.Error("TelemetryEnabled should still be true after null update")
|
|
}
|
|
|
|
// Verify persisted value is still set (not nil).
|
|
saved, err := persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.TelemetryEnabled == nil {
|
|
t.Error("persisted TelemetryEnabled should not be nil after null update")
|
|
} else if !*saved.TelemetryEnabled {
|
|
t.Error("persisted TelemetryEnabled should still be true")
|
|
}
|
|
}
|
|
|
|
func TestTelemetryUpdate_PersistBeforeMutate(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
PVEPollingInterval: 10 * time.Second,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
|
|
saveCalled := false
|
|
persistence := config.NewConfigPersistence(cfg.DataPath)
|
|
tokenVal := "telemetry-test-token-123.12345678"
|
|
tokenHash := internalauth.HashAPIToken(tokenVal)
|
|
cfg.APITokens = []config.APITokenRecord{
|
|
{ID: "tok1", Hash: tokenHash, Name: "Test Token"},
|
|
}
|
|
handler := newTestSystemSettingsHandler(cfg, persistence, &mockMonitor{}, func() {
|
|
saveCalled = true
|
|
}, func() error { return nil })
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{"telemetryEnabled": false})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", tokenVal)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
if !saveCalled {
|
|
t.Error("expected reload (post-save) to be called")
|
|
}
|
|
|
|
// In-memory should now be false (applied after successful save).
|
|
if cfg.TelemetryEnabled {
|
|
t.Error("TelemetryEnabled should be false after successful update")
|
|
}
|
|
}
|
|
|
|
func TestTelemetryUpdate_GetReturnsEffectiveValue(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: map[string]bool{"PULSE_TELEMETRY": true},
|
|
}
|
|
handler, persistence, _ := setupTelemetryTest(t, cfg)
|
|
|
|
// Persist with telemetry disabled (simulating a stale disk value).
|
|
initial := config.DefaultSystemSettings()
|
|
disabled := false
|
|
initial.TelemetryEnabled = &disabled
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/system-settings", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
|
|
var response struct {
|
|
TelemetryEnabled *bool `json:"telemetryEnabled"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
|
|
if response.TelemetryEnabled == nil {
|
|
t.Fatal("telemetryEnabled should be present in response")
|
|
}
|
|
if !*response.TelemetryEnabled {
|
|
t.Error("GET should return effective runtime value (true), not stale disk value (false)")
|
|
}
|
|
}
|
|
|
|
func TestTelemetryPreview_ReturnsCurrentPayload(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: false,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, _, _ := setupTelemetryTest(t, cfg)
|
|
handler.SetTelemetryPreviewFunc(func() (telemetry.Ping, error) {
|
|
return telemetry.Ping{
|
|
InstallID: "preview-install-id",
|
|
Version: "6.0.0",
|
|
Event: "heartbeat",
|
|
Platform: "docker",
|
|
OS: "linux",
|
|
Arch: "amd64",
|
|
}, nil
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/system/settings/telemetry-preview", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleGetTelemetryPreview(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var response TelemetryPreviewResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if response.Enabled {
|
|
t.Fatal("expected telemetry preview to reflect disabled runtime state")
|
|
}
|
|
if response.Payload.InstallID != "preview-install-id" {
|
|
t.Fatalf("install_id = %q, want preview-install-id", response.Payload.InstallID)
|
|
}
|
|
if response.Payload.Event != "heartbeat" {
|
|
t.Fatalf("event = %q, want heartbeat", response.Payload.Event)
|
|
}
|
|
}
|
|
|
|
func TestTelemetryReset_ReturnsUpdatedPayload(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, _, token := setupTelemetryTest(t, cfg)
|
|
handler.SetTelemetryResetFunc(func() (telemetry.Ping, error) {
|
|
return telemetry.Ping{
|
|
InstallID: "rotated-install-id",
|
|
Version: "6.0.0",
|
|
Event: "heartbeat",
|
|
Platform: "binary",
|
|
OS: "linux",
|
|
Arch: "amd64",
|
|
}, nil
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/telemetry-reset-id", nil)
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleResetTelemetryID(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var response TelemetryPreviewResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !response.Enabled {
|
|
t.Fatal("expected telemetry reset response to reflect enabled runtime state")
|
|
}
|
|
if response.Payload.InstallID != "rotated-install-id" {
|
|
t.Fatalf("install_id = %q, want rotated-install-id", response.Payload.InstallID)
|
|
}
|
|
}
|
|
|
|
func TestTelemetryUpdate_NoMutationOnPersistFailure(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
PVEPollingInterval: 10 * time.Second,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Make the config directory read-only so SaveSystemSettings fails.
|
|
systemFile := filepath.Join(tempDir, "system.json")
|
|
if err := os.Chmod(systemFile, 0400); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(tempDir, 0500); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
os.Chmod(tempDir, 0700)
|
|
os.Chmod(systemFile, 0600)
|
|
})
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{"telemetryEnabled": false})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected 500 when persistence fails, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// In-memory config must NOT have been mutated.
|
|
if !cfg.TelemetryEnabled {
|
|
t.Error("TelemetryEnabled should still be true after persistence failure")
|
|
}
|
|
}
|
|
|
|
func TestTelemetryUpdate_UnrelatedUpdateDoesNotToggle(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
// Persist with telemetry enabled.
|
|
initial := config.DefaultSystemSettings()
|
|
enabled := true
|
|
initial.TelemetryEnabled = &enabled
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Track whether toggle callback fires.
|
|
toggleCalled := false
|
|
handler.SetTelemetryToggleFunc(func(en bool) {
|
|
toggleCalled = true
|
|
})
|
|
|
|
// Send an update that does NOT include telemetryEnabled.
|
|
body, _ := json.Marshal(map[string]interface{}{"theme": "dark"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if toggleCalled {
|
|
t.Error("telemetry toggle callback should NOT fire for unrelated settings updates")
|
|
}
|
|
}
|
|
|
|
func TestCIDRUpdate_InvalidCIDRRejectedBeforePersist(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
initial.WebhookAllowedPrivateCIDRs = "192.168.1.0/24"
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Send an invalid CIDR value — should be rejected with 400 before persisting.
|
|
body, _ := json.Marshal(map[string]interface{}{"webhookAllowedPrivateCIDRs": "not-a-cidr"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid CIDR, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Verify persisted value was NOT changed.
|
|
saved, err := persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.WebhookAllowedPrivateCIDRs != "192.168.1.0/24" {
|
|
t.Errorf("persisted CIDRs should be unchanged, got %q", saved.WebhookAllowedPrivateCIDRs)
|
|
}
|
|
}
|
|
|
|
func TestCIDRUpdate_ValidCIDRPersisted(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
cfg := &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
TelemetryEnabled: true,
|
|
EnvOverrides: make(map[string]bool),
|
|
}
|
|
handler, persistence, token := setupTelemetryTest(t, cfg)
|
|
|
|
initial := config.DefaultSystemSettings()
|
|
if err := persistence.SaveSystemSettings(*initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Send a valid CIDR value.
|
|
body, _ := json.Marshal(map[string]interface{}{"webhookAllowedPrivateCIDRs": "10.0.0.0/8, 172.16.0.0/12"})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system-settings", bytes.NewReader(body))
|
|
req.Header.Set("X-API-Token", token)
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.HandleUpdateSystemSettings(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
// Verify persisted value was updated.
|
|
saved, err := persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.WebhookAllowedPrivateCIDRs != "10.0.0.0/8, 172.16.0.0/12" {
|
|
t.Errorf("persisted CIDRs should be updated, got %q", saved.WebhookAllowedPrivateCIDRs)
|
|
}
|
|
}
|