2026-02-25 18:54:22 +00:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
2026-04-16 12:57:06 +00:00
|
|
|
"time"
|
2026-02-25 18:54:22 +00:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2026-04-16 12:57:06 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"go.uber.org/zap/zaptest"
|
|
|
|
|
"gorm.io/driver/sqlite"
|
|
|
|
|
"gorm.io/gorm"
|
2026-02-25 18:54:22 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestMaintenanceGin_Disabled(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
SetMaintenanceMode(false)
|
|
|
|
|
defer SetMaintenanceMode(false)
|
|
|
|
|
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Use(MaintenanceGin())
|
|
|
|
|
router.GET("/api/v1/dashboard", func(c *gin.Context) {
|
|
|
|
|
c.Status(http.StatusOK)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/dashboard", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMaintenanceGin_Enabled_Returns503(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
SetMaintenanceMode(true)
|
|
|
|
|
defer SetMaintenanceMode(false)
|
|
|
|
|
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Use(MaintenanceGin())
|
|
|
|
|
router.GET("/api/v1/dashboard", func(c *gin.Context) {
|
|
|
|
|
c.Status(http.StatusOK)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/dashboard", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
|
|
|
assert.Contains(t, w.Body.String(), "maintenance")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMaintenanceGin_HealthExempt(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
SetMaintenanceMode(true)
|
|
|
|
|
defer SetMaintenanceMode(false)
|
|
|
|
|
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Use(MaintenanceGin())
|
|
|
|
|
router.GET("/health", func(c *gin.Context) {
|
|
|
|
|
c.Status(http.StatusOK)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/health", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMaintenanceGin_AdminExempt(t *testing.T) {
|
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
|
SetMaintenanceMode(true)
|
|
|
|
|
defer SetMaintenanceMode(false)
|
|
|
|
|
|
|
|
|
|
router := gin.New()
|
|
|
|
|
router.Use(MaintenanceGin())
|
|
|
|
|
router.GET("/api/v1/admin/reports", func(c *gin.Context) {
|
|
|
|
|
c.Status(http.StatusOK)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
req := httptest.NewRequest("GET", "/api/v1/admin/reports", nil)
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
|
}
|
2026-04-16 12:57:06 +00:00
|
|
|
|
|
|
|
|
// TestMaintenanceGin_DBBacked verifies that changes written to
|
|
|
|
|
// platform_settings propagate to MaintenanceModeEnabled() once the cache TTL
|
|
|
|
|
// lapses. This guards the multi-pod correctness claim of v1.0.4.
|
|
|
|
|
func TestMaintenanceGin_DBBacked(t *testing.T) {
|
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, db.Exec(`
|
|
|
|
|
CREATE TABLE platform_settings (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
key TEXT NOT NULL UNIQUE,
|
|
|
|
|
value_bool BOOLEAN,
|
|
|
|
|
value_text TEXT,
|
|
|
|
|
description TEXT,
|
|
|
|
|
updated_at DATETIME,
|
|
|
|
|
updated_by TEXT
|
|
|
|
|
)`).Error)
|
|
|
|
|
require.NoError(t, db.Exec(
|
|
|
|
|
`INSERT INTO platform_settings (key, value_bool, description) VALUES ('maintenance_mode', 0, 'test')`,
|
|
|
|
|
).Error)
|
|
|
|
|
|
|
|
|
|
// Start from a clean slate so no prior test leaked state into the package
|
|
|
|
|
// globals.
|
|
|
|
|
SetMaintenanceMode(false)
|
|
|
|
|
defer SetMaintenanceMode(false)
|
|
|
|
|
|
|
|
|
|
InitMaintenanceMode(db, zaptest.NewLogger(t))
|
|
|
|
|
// Shrink the TTL so we don't have to sleep 10s.
|
|
|
|
|
state.mu.Lock()
|
|
|
|
|
state.ttl = 50 * time.Millisecond
|
|
|
|
|
state.mu.Unlock()
|
|
|
|
|
defer func() {
|
|
|
|
|
state.mu.Lock()
|
|
|
|
|
state.ttl = defaultMaintenanceCacheTTL
|
|
|
|
|
state.db = nil
|
|
|
|
|
state.mu.Unlock()
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
assert.False(t, MaintenanceModeEnabled(), "seeded value=0 should read as off")
|
|
|
|
|
|
|
|
|
|
// Flip the DB row; before TTL the cached value still says off.
|
|
|
|
|
require.NoError(t, db.Exec(
|
|
|
|
|
`UPDATE platform_settings SET value_bool = 1 WHERE key = 'maintenance_mode'`,
|
|
|
|
|
).Error)
|
|
|
|
|
assert.False(t, MaintenanceModeEnabled(), "cache should still report off before TTL")
|
|
|
|
|
|
|
|
|
|
time.Sleep(70 * time.Millisecond)
|
|
|
|
|
assert.True(t, MaintenanceModeEnabled(), "after TTL the refresh should pick up the new value")
|
|
|
|
|
}
|