Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
Ran gofmt across the entire mothership codebase to ensure consistent code formatting per Go standards. All tests pass after formatting.
715 lines
19 KiB
Go
715 lines
19 KiB
Go
// Package api provides tests for the settings API handler.
|
|
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// TestSettingsHandler tests the settings handler with table-driven tests.
|
|
func TestSettingsHandler(t *testing.T) {
|
|
// Create a temporary database
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
defer db.Close() //nolint:errcheck
|
|
|
|
// Create settings table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create settings table: %v", err)
|
|
}
|
|
|
|
// Insert some default settings
|
|
_, err = db.Exec(`
|
|
INSERT INTO settings (key, value_json, updated_at) VALUES
|
|
('fusion_rate_hz', '10', 1000),
|
|
('delta_rms_threshold', '0.02', 1000)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert default settings: %v", err)
|
|
}
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body interface{}
|
|
expectedStatus int
|
|
checkResponse func(*testing.T, *httptest.ResponseRecorder)
|
|
}{
|
|
{
|
|
name: "GET settings returns all settings with defaults",
|
|
method: "GET",
|
|
path: "/api/settings",
|
|
expectedStatus: http.StatusOK,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var settings map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&settings); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
// Check that we have values from DB and defaults
|
|
if v, ok := settings["fusion_rate_hz"]; !ok || v.(float64) != 10.0 {
|
|
t.Errorf("Expected fusion_rate_hz=10.0, got %v", v)
|
|
}
|
|
if v, ok := settings["delta_rms_threshold"]; !ok || v.(float64) != 0.02 {
|
|
t.Errorf("Expected delta_rms_threshold=0.02, got %v", v)
|
|
}
|
|
if v, ok := settings["grid_cell_m"]; !ok || v.(float64) != 0.2 {
|
|
t.Errorf("Expected default grid_cell_m=0.2, got %v", v)
|
|
}
|
|
if v, ok := settings["tau_s"]; !ok || v.(float64) != 30.0 {
|
|
t.Errorf("Expected default tau_s=30.0, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST single setting update",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"fusion_rate_hz": 15.0,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var settings map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&settings); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if v, ok := settings["fusion_rate_hz"]; !ok || v.(float64) != 15.0 {
|
|
t.Errorf("Expected fusion_rate_hz=15.0, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST multiple settings update",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"fusion_rate_hz": 12.0,
|
|
"delta_rms_threshold": 0.03,
|
|
"grid_cell_m": 0.15,
|
|
"max_tracked_blobs": 30,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var settings map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&settings); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if v, ok := settings["fusion_rate_hz"]; !ok || v.(float64) != 12.0 {
|
|
t.Errorf("Expected fusion_rate_hz=12.0, got %v", v)
|
|
}
|
|
if v, ok := settings["delta_rms_threshold"]; !ok || v.(float64) != 0.03 {
|
|
t.Errorf("Expected delta_rms_threshold=0.03, got %v", v)
|
|
}
|
|
if v, ok := settings["grid_cell_m"]; !ok || v.(float64) != 0.15 {
|
|
t.Errorf("Expected grid_cell_m=0.15, got %v", v)
|
|
}
|
|
if v, ok := settings["max_tracked_blobs"]; !ok {
|
|
t.Errorf("Expected max_tracked_blobs=30, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "PATCH settings (same as POST)",
|
|
method: "PATCH",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"security_mode": true,
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var settings map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&settings); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if v, ok := settings["security_mode"]; !ok || v.(bool) != true {
|
|
t.Errorf("Expected security_mode=true, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid fusion_rate_hz (too high)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"fusion_rate_hz": 100.0,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid delta_rms_threshold (negative)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"delta_rms_threshold": -0.01,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid grid_cell_m (too small)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"grid_cell_m": 0.01,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid n_subcarriers (out of range)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"n_subcarriers": 50,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid max_tracked_blobs (too high)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"max_tracked_blobs": 200,
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "POST invalid security_mode (not boolean)",
|
|
method: "POST",
|
|
path: "/api/settings",
|
|
body: map[string]interface{}{
|
|
"security_mode": "true",
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
var errResp map[string]string
|
|
if err := json.NewDecoder(rr.Body).Decode(&errResp); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode error response: %v", err)
|
|
}
|
|
|
|
if errResp["error"] == "" {
|
|
t.Error("Expected error message, got empty string")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "GET settings after update persists changes",
|
|
method: "GET",
|
|
path: "/api/settings",
|
|
expectedStatus: http.StatusOK,
|
|
checkResponse: func(t *testing.T, rr *httptest.ResponseRecorder) {
|
|
// This test should run after the POST test above
|
|
// Just verify we can get settings without error
|
|
var settings map[string]interface{}
|
|
if err := json.NewDecoder(rr.Body).Decode(&settings); err != nil { //nolint:errcheck
|
|
t.Fatalf("Failed to decode response: %v", err)
|
|
}
|
|
|
|
if len(settings) == 0 {
|
|
t.Error("Expected at least some settings")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := chi.NewRouter()
|
|
handler.RegisterRoutes(r)
|
|
|
|
var body []byte
|
|
if tt.body != nil {
|
|
var err error
|
|
body, err = json.Marshal(tt.body)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal request body: %v", err)
|
|
}
|
|
}
|
|
|
|
req := httptest.NewRequest(tt.method, tt.path, bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
rr := httptest.NewRecorder()
|
|
r.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != tt.expectedStatus {
|
|
t.Errorf("Expected status %d, got %d", tt.expectedStatus, rr.Code)
|
|
t.Logf("Response body: %s", rr.Body.String())
|
|
}
|
|
|
|
if tt.checkResponse != nil {
|
|
tt.checkResponse(t, rr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSettingsGetSingle tests the GetSingle method.
|
|
func TestSettingsGetSingle(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
defer db.Close() //nolint:errcheck
|
|
|
|
// Create settings table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create settings table: %v", err)
|
|
}
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
wantExists bool
|
|
checkValue func(*testing.T, interface{})
|
|
}{
|
|
{
|
|
name: "cached value exists",
|
|
key: "fusion_rate_hz",
|
|
wantExists: true,
|
|
checkValue: func(t *testing.T, v interface{}) {
|
|
if f, ok := v.(float64); !ok || f != 10.0 {
|
|
t.Errorf("Expected 10.0, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "default value exists",
|
|
key: "grid_cell_m",
|
|
wantExists: true,
|
|
checkValue: func(t *testing.T, v interface{}) {
|
|
if f, ok := v.(float64); !ok || f != 0.2 {
|
|
t.Errorf("Expected 0.2, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "unknown key doesn't exist",
|
|
key: "unknown_key_xyz",
|
|
wantExists: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
val, exists := handler.GetSingle(tt.key)
|
|
if exists != tt.wantExists {
|
|
t.Errorf("GetSingle(%q) exists=%v, want %v", tt.key, exists, tt.wantExists)
|
|
}
|
|
if tt.checkValue != nil && exists {
|
|
tt.checkValue(t, val)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSettingsSetAndGet tests setting and getting a single value.
|
|
func TestSettingsSetAndGet(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
defer db.Close() //nolint:errcheck
|
|
|
|
// Create settings table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create settings table: %v", err)
|
|
}
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
// Set a new value
|
|
err = handler.Set("custom_key", "custom_value")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set value: %v", err)
|
|
}
|
|
|
|
// Get it back
|
|
val, exists := handler.GetSingle("custom_key")
|
|
if !exists {
|
|
t.Fatal("Value should exist after Set")
|
|
}
|
|
if val != "custom_value" {
|
|
t.Errorf("Expected 'custom_value', got %v", val)
|
|
}
|
|
|
|
// Verify it's in the database
|
|
var dbVal string
|
|
err = db.QueryRow("SELECT value_json FROM settings WHERE key = ?", "custom_key").Scan(&dbVal)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query database: %v", err)
|
|
}
|
|
if dbVal != `"custom_value"` { // JSON encoded string
|
|
t.Errorf("Expected '\"custom_value\"' in database, got %s", dbVal)
|
|
}
|
|
}
|
|
|
|
// TestSettingsDelete tests deleting a setting.
|
|
func TestSettingsDelete(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
defer db.Close() //nolint:errcheck
|
|
|
|
// Create settings table
|
|
_, err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create settings table: %v", err)
|
|
}
|
|
|
|
handler := NewSettingsHandler(db)
|
|
|
|
// Set a value
|
|
err = handler.Set("to_delete", "value")
|
|
if err != nil {
|
|
t.Fatalf("Failed to set value: %v", err)
|
|
}
|
|
|
|
// Verify it exists
|
|
_, exists := handler.GetSingle("to_delete")
|
|
if !exists {
|
|
t.Fatal("Value should exist before delete")
|
|
}
|
|
|
|
// Delete it
|
|
err = handler.Delete("to_delete")
|
|
if err != nil {
|
|
t.Fatalf("Failed to delete: %v", err)
|
|
}
|
|
|
|
// Verify it's gone from cache
|
|
_, exists = handler.GetSingle("to_delete")
|
|
if exists {
|
|
t.Fatal("Value should not exist after delete")
|
|
}
|
|
|
|
// Verify it's gone from database
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM settings WHERE key = ?", "to_delete").Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("Failed to query database: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("Expected 0 rows in database, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestValidateSettings tests the settings validation.
|
|
func TestValidateSettings(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
settings map[string]interface{}
|
|
wantErr bool
|
|
errKey string
|
|
}{
|
|
{
|
|
name: "all valid settings",
|
|
settings: map[string]interface{}{
|
|
"fusion_rate_hz": 10.0,
|
|
"grid_cell_m": 0.2,
|
|
"delta_rms_threshold": 0.02,
|
|
"tau_s": 30.0,
|
|
"fresnel_decay": 2.0,
|
|
"n_subcarriers": 16,
|
|
"breathing_sensitivity": 0.005,
|
|
"motion_threshold": 0.05,
|
|
"dwell_seconds": 30,
|
|
"vacant_seconds": 300,
|
|
"max_tracked_blobs": 20,
|
|
"security_mode": true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "fusion_rate_hz too high",
|
|
settings: map[string]interface{}{
|
|
"fusion_rate_hz": 100.0,
|
|
},
|
|
wantErr: true,
|
|
errKey: "fusion_rate_hz",
|
|
},
|
|
{
|
|
name: "grid_cell_m too small",
|
|
settings: map[string]interface{}{
|
|
"grid_cell_m": 0.01,
|
|
},
|
|
wantErr: true,
|
|
errKey: "grid_cell_m",
|
|
},
|
|
{
|
|
name: "n_subcarriers out of range",
|
|
settings: map[string]interface{}{
|
|
"n_subcarriers": 50,
|
|
},
|
|
wantErr: true,
|
|
errKey: "n_subcarriers",
|
|
},
|
|
{
|
|
name: "security_mode not boolean",
|
|
settings: map[string]interface{}{
|
|
"security_mode": "true",
|
|
},
|
|
wantErr: true,
|
|
errKey: "security_mode",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateSettings(tt.settings)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateSettings() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if err != nil && tt.errKey != "" {
|
|
if ve, ok := err.(*ValidationError); ok {
|
|
if ve.Key != tt.errKey {
|
|
t.Errorf("Expected error key %q, got %q", tt.errKey, ve.Key)
|
|
}
|
|
} else {
|
|
t.Errorf("Expected ValidationError, got %T", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAsFloat64 tests the asFloat64 helper.
|
|
func TestAsFloat64(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input interface{}
|
|
want float64
|
|
wantBool bool
|
|
}{
|
|
{"float64", 3.14, 3.14, true},
|
|
{"float32", float32(3.14), float64(float32(3.14)), true},
|
|
{"int", 42, 42.0, true},
|
|
{"int64", int64(42), 42.0, true},
|
|
{"int32", int32(42), 42.0, true},
|
|
{"string", "42", 0, false},
|
|
{"bool", true, 0, false},
|
|
{"nil", nil, 0, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := asFloat64(tt.input)
|
|
if ok != tt.wantBool {
|
|
t.Errorf("asFloat64(%v) ok = %v, want %v", tt.input, ok, tt.wantBool)
|
|
}
|
|
if ok && got != tt.want {
|
|
t.Errorf("asFloat64(%v) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDefaultSettings tests that default settings are defined.
|
|
func TestDefaultSettings(t *testing.T) {
|
|
requiredDefaults := []string{
|
|
"fusion_rate_hz",
|
|
"grid_cell_m",
|
|
"delta_rms_threshold",
|
|
"tau_s",
|
|
"fresnel_decay",
|
|
"n_subcarriers",
|
|
"breathing_sensitivity",
|
|
"motion_threshold",
|
|
"dwell_seconds",
|
|
"vacant_seconds",
|
|
"max_tracked_blobs",
|
|
"replay_retention_hours",
|
|
"replay_max_mb",
|
|
"security_mode",
|
|
"events_archive_days",
|
|
}
|
|
|
|
for _, key := range requiredDefaults {
|
|
if _, exists := defaultSettings[key]; !exists {
|
|
t.Errorf("Missing default setting: %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSettingsPersistence tests that settings persist across handler reloads.
|
|
func TestSettingsPersistence(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
// First handler
|
|
db1, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
|
|
_, err = db1.Exec(`
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value_json TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
`)
|
|
if err != nil {
|
|
db1.Close() //nolint:errcheck
|
|
t.Fatalf("Failed to create settings table: %v", err)
|
|
}
|
|
|
|
handler1 := NewSettingsHandler(db1)
|
|
err = handler1.Set("persistent_key", "persistent_value")
|
|
if err != nil {
|
|
db1.Close() //nolint:errcheck
|
|
t.Fatalf("Failed to set value: %v", err)
|
|
}
|
|
db1.Close() //nolint:errcheck
|
|
|
|
// Second handler (simulates restart)
|
|
db2, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to reopen database: %v", err)
|
|
}
|
|
defer db2.Close() //nolint:errcheck
|
|
|
|
handler2 := NewSettingsHandler(db2)
|
|
val, exists := handler2.GetSingle("persistent_key")
|
|
if !exists {
|
|
t.Fatal("Value should persist across handler reloads")
|
|
}
|
|
if val != "persistent_value" {
|
|
t.Errorf("Expected 'persistent_value', got %v", val)
|
|
}
|
|
}
|
|
|
|
// TestNewSettingsHandlerLoadFailure tests that handler still works even if load fails.
|
|
func TestNewSettingsHandlerLoadFailure(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "test.db")
|
|
|
|
// Create a database without the settings table
|
|
db, err := sql.Open("sqlite", dbPath+"?mode=ro") // Read-only to prevent table creation
|
|
if err != nil {
|
|
t.Fatalf("Failed to open database: %v", err)
|
|
}
|
|
defer db.Close() //nolint:errcheck
|
|
|
|
// Handler should still be created (load fails but doesn't crash)
|
|
handler := NewSettingsHandler(db)
|
|
|
|
// Get should return defaults even though load failed
|
|
settings := handler.Get()
|
|
if len(settings) == 0 {
|
|
t.Error("Expected default settings even with failed load")
|
|
}
|
|
}
|
|
|
|
// cleanTestFile removes a test file if it exists.
|
|
func cleanTestFile(path string) {
|
|
os.Remove(path)
|
|
}
|