Pulse/internal/api/system_settings_telemetry_test.go
rcourtman b707512e38 Clear all errcheck and gofmt violations so make lint gates on real findings
golangci-lint run ./... failed on ~190 pre-existing errcheck violations and
5 unformatted files, burying any new regression in noise. Fix all of them:

- Test files that hand-rolled mock-mode set/restore (vmware, truenas, and
  friends) now use the canonical setMockModeForTest/testutil.SetMockMode
  helper instead of drift copies that ignored SetEnabled errors.
- internal/mock and internal/monitoring tests get package-local
  mustSetEnabled/mustSetMockEnabled/mustSetMonitorMockMode helpers that
  fail the test on toggle errors.
- pkg/auth/sqlite_manager.go, pkg/metrics/store.go, pkg/server/server.go:
  rollbacks in defers use the explicit-discard idiom, migration renames and
  rollup commits log failures, the hosted reaper goroutine logs an error
  exit, shutdown mock-disable logs failures.
- Remaining test sites check errors with t.Fatalf/t.Errorf or explicitly
  discard best-effort calls (restore-chmods, handler-closure unmarshals)
  per existing repo style.
- gofmt: internal/api/maintenance_verification.go, internal/ai/demo.go and
  three findings test files.

Only dupl findings remain (44 pre-existing production-code duplication
pairs) — those need real refactors, not mechanical fixes.

Full test suites pass for every touched package.
2026-06-09 21:42:21 +01:00

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) // restore so TempDir cleanup works
_ = os.Chmod(systemFile, 0600) // restore so TempDir cleanup works
})
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)
}
}