Files
OpenFlare/backend/openflare/plugins/server/domain/option/uptimekuma/sync_test.go
T

369 lines
11 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package uptimekuma
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"Wavelet/openflare/plugins/server/kernel/model"
"Wavelet/openflare/plugins/server/kernel/repository"
"Wavelet/openflare/plugins/server/kernel/testhelper"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
type mockKumaServer struct {
mu sync.Mutex
postsReceived []string
pendingPackets chan string
monitorList string
}
func newMockKumaServer(monitorList string) *mockKumaServer {
return &mockKumaServer{
pendingPackets: make(chan string, 100),
monitorList: monitorList,
}
}
func (s *mockKumaServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
transport := r.URL.Query().Get("transport")
sid := r.URL.Query().Get("sid")
if r.Method == http.MethodGet {
if transport == "polling" && sid == "" {
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
_, _ = w.Write([]byte(`0{"sid":"mock-sid"}`))
return
}
if transport == "polling" && sid == "mock-sid" {
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
select {
case pkt := <-s.pendingPackets:
_, _ = w.Write([]byte(pkt))
case <-time.After(100 * time.Millisecond):
_, _ = w.Write([]byte(""))
}
return
}
} else if r.Method == http.MethodPost {
bodyBytes, _ := io.ReadAll(r.Body)
bodyStr := string(bodyBytes)
s.postsReceived = append(s.postsReceived, bodyStr)
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
w.WriteHeader(http.StatusOK)
if bodyStr == "40" {
s.pendingPackets <- fmt.Sprintf(`42["monitorList",%s]`, s.monitorList)
return
}
if strings.HasPrefix(bodyStr, "42") {
payload := bodyStr[2:]
digitsEnd := 0
for digitsEnd < len(payload) && payload[digitsEnd] >= '0' && payload[digitsEnd] <= '9' {
digitsEnd++
}
if digitsEnd == 0 {
return
}
ackIDStr := payload[:digitsEnd]
jsonArrayStr := payload[digitsEnd:]
var arr []json.RawMessage
if err := json.Unmarshal([]byte(jsonArrayStr), &arr); err != nil || len(arr) == 0 {
return
}
var eventName string
_ = json.Unmarshal(arr[0], &eventName)
switch eventName {
case "login", "loginByToken":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true}]", ackIDStr)
case "getTags":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true,\"tags\":[{\"id\":10,\"name\":\"OpenFlare\",\"color\":\"#4f46e5\"}]}]", ackIDStr)
case "addTag":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true,\"tag\":{\"id\":10}}]", ackIDStr)
case "add":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true,\"monitorID\":100}]", ackIDStr)
case "addMonitorTag":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true}]", ackIDStr)
case "editMonitor":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true}]", ackIDStr)
case "deleteMonitor":
s.pendingPackets <- fmt.Sprintf("43%s[{\"ok\":true}]", ackIDStr)
}
}
}
}
func setupSyncTestDB(t *testing.T) func() {
t.Helper()
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
require.NoError(t, err)
require.NoError(t, sqliteDB.AutoMigrate(&model.ProxyRoute{}, &model.Zone{}, &model.ZoneDomain{}, &model.SystemConfig{}))
repository.SetDBForTest(sqliteDB)
repository.SetSystemConfigService(testhelper.NewMockSystemConfigService(sqliteDB))
return func() {
repository.SetSystemConfigService(nil)
repository.SetDBForTest(nil)
}
}
func createRouteZoneDomain(t *testing.T, ctx context.Context, route *model.ProxyRoute, domain string) {
t.Helper()
zone := &model.Zone{Domain: fmt.Sprintf("zone-%d.example", route.ID)}
require.NoError(t, repository.DB(ctx).Create(zone).Error)
require.NoError(t, repository.DB(ctx).Create(&model.ZoneDomain{
ZoneID: zone.ID,
ProxyRouteID: &route.ID,
Domain: domain,
}).Error)
}
func backupUptimeKumaConfig(ctx context.Context) func() {
// 备份所有 UptimeKuma 相关配置
configs := []string{
model.ConfigKeyUptimeKumaEnabled,
model.ConfigKeyUptimeKumaURL,
model.ConfigKeyUptimeKumaUsername,
model.ConfigKeyUptimeKumaPassword,
model.ConfigKeyUptimeKumaMonitorScope,
model.ConfigKeyUptimeKumaSelectedSites,
model.ConfigKeyUptimeKumaInterval,
model.ConfigKeyUptimeKumaRetry,
model.ConfigKeyUptimeKumaRetryInterval,
model.ConfigKeyUptimeKumaTimeout,
}
oldValues := make(map[string]string)
for _, key := range configs {
config, _ := repository.GetSystemConfigByKey(ctx, key)
oldValues[key] = config.Value
}
return func() {
// 恢复所有配置
for key, value := range oldValues {
_ = repository.DB(ctx).Model(&model.SystemConfig{}).Where("key = ?", key).Update("value", value).Error
}
}
}
// setTestConfig 设置测试配置的辅助函数(不存在则创建)
func setTestConfig(ctx context.Context, key, value string) {
_ = repository.SaveOrUpdateSystemConfig(ctx, key, value)
}
func TestSyncToUptimeKumaDisabled(t *testing.T) {
cleanup := setupSyncTestDB(t)
defer cleanup()
ctx := context.Background()
restore := backupUptimeKumaConfig(ctx)
defer restore()
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "false")
err := SyncToUptimeKuma(ctx)
require.Error(t, err)
assert.Contains(t, err.Error(), "disabled")
}
func TestSyncToUptimeKumaSuccess(t *testing.T) {
cleanup := setupSyncTestDB(t)
defer cleanup()
ctx := context.Background()
restore := backupUptimeKumaConfig(ctx)
defer restore()
require.NoError(t, repository.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error)
routeA := &model.ProxyRoute{
SiteName: "site-a",
OriginURL: "http://10.0.0.1",
Enabled: true,
EnableHTTPS: false,
}
routeB := &model.ProxyRoute{
SiteName: "site-b",
OriginURL: "https://10.0.0.2",
Enabled: true,
EnableHTTPS: true,
}
routeC := &model.ProxyRoute{
SiteName: "site-c",
OriginURL: "http://10.0.0.3",
Enabled: false,
EnableHTTPS: false,
}
require.NoError(t, repository.CreateProxyRouteRecord(ctx, routeA))
require.NoError(t, repository.CreateProxyRouteRecord(ctx, routeB))
require.NoError(t, repository.CreateProxyRouteRecord(ctx, routeC))
createRouteZoneDomain(t, ctx, routeA, "site-a.com")
createRouteZoneDomain(t, ctx, routeB, "site-b.com")
createRouteZoneDomain(t, ctx, routeC, "site-c.com")
monitorListJSON := `{
"99": {
"id": 99,
"name": "site-old",
"url": "http://site-old.com",
"interval": 60,
"tags": [{"tag_id": 10, "name": "OpenFlare"}]
},
"98": {
"id": 98,
"name": "site-a",
"url": "http://site-a.com",
"interval": 30,
"tags": [{"tag_id": 10, "name": "OpenFlare"}]
}
}`
mockSrv := newMockKumaServer(monitorListJSON)
server := httptest.NewServer(mockSrv)
defer server.Close()
// 设置测试配置
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true")
setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL)
setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin")
setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password")
setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "all")
setTestConfig(ctx, model.ConfigKeyUptimeKumaInterval, "60")
setTestConfig(ctx, model.ConfigKeyUptimeKumaRetry, "0")
setTestConfig(ctx, model.ConfigKeyUptimeKumaRetryInterval, "60")
setTestConfig(ctx, model.ConfigKeyUptimeKumaTimeout, "48")
require.NoError(t, SyncToUptimeKuma(ctx))
mockSrv.mu.Lock()
posts := mockSrv.postsReceived
mockSrv.mu.Unlock()
hasLogin := false
hasGetTags := false
hasAddSiteB := false
hasTagSiteB := false
hasEditSiteA := false
hasDeleteOld := false
for _, body := range posts {
if strings.Contains(body, `"login"`) && strings.Contains(body, `"admin"`) && strings.Contains(body, `"password"`) {
hasLogin = true
}
if strings.Contains(body, `"getTags"`) {
hasGetTags = true
}
if strings.Contains(body, `"add"`) && strings.Contains(body, `"site-b"`) && strings.Contains(body, `"https://site-b.com"`) {
hasAddSiteB = true
}
if strings.Contains(body, `"addMonitorTag"`) && strings.Contains(body, `10`) && strings.Contains(body, `100`) {
hasTagSiteB = true
}
if strings.Contains(body, `"editMonitor"`) && strings.Contains(body, `98`) && strings.Contains(body, `"site-a"`) && strings.Contains(body, `"interval":60`) {
hasEditSiteA = true
}
if strings.Contains(body, `"deleteMonitor"`) && strings.Contains(body, `99`) {
hasDeleteOld = true
}
}
assert.True(t, hasLogin, "expected login event to be called")
assert.True(t, hasGetTags, "expected getTags event to be called")
assert.True(t, hasAddSiteB, "expected site-b to be added")
assert.True(t, hasTagSiteB, "expected site-b to be tagged")
assert.True(t, hasEditSiteA, "expected site-a to be edited/updated")
assert.True(t, hasDeleteOld, "expected site-old to be deleted")
}
func TestSyncToUptimeKumaSelectedScope(t *testing.T) {
cleanup := setupSyncTestDB(t)
defer cleanup()
ctx := context.Background()
restore := backupUptimeKumaConfig(ctx)
defer restore()
require.NoError(t, repository.DB(ctx).Where("1 = 1").Delete(&model.ProxyRoute{}).Error)
routeA := &model.ProxyRoute{
SiteName: "site-a",
OriginURL: "http://10.0.0.1",
Enabled: true,
EnableHTTPS: false,
}
routeB := &model.ProxyRoute{
SiteName: "site-b",
OriginURL: "http://10.0.0.2",
Enabled: true,
EnableHTTPS: false,
}
require.NoError(t, repository.CreateProxyRouteRecord(ctx, routeA))
require.NoError(t, repository.CreateProxyRouteRecord(ctx, routeB))
createRouteZoneDomain(t, ctx, routeA, "site-a.com")
createRouteZoneDomain(t, ctx, routeB, "site-b.com")
mockSrv := newMockKumaServer(`{}`)
server := httptest.NewServer(mockSrv)
defer server.Close()
// 设置测试配置
setTestConfig(ctx, model.ConfigKeyUptimeKumaEnabled, "true")
setTestConfig(ctx, model.ConfigKeyUptimeKumaURL, server.URL)
setTestConfig(ctx, model.ConfigKeyUptimeKumaUsername, "admin")
setTestConfig(ctx, model.ConfigKeyUptimeKumaPassword, "password")
setTestConfig(ctx, model.ConfigKeyUptimeKumaMonitorScope, "selected")
setTestConfig(ctx, model.ConfigKeyUptimeKumaSelectedSites, "site-a")
require.NoError(t, SyncToUptimeKuma(ctx))
mockSrv.mu.Lock()
posts := mockSrv.postsReceived
mockSrv.mu.Unlock()
hasLogin := false
hasAddSiteA := false
hasAddSiteB := false
for _, body := range posts {
if strings.Contains(body, `"login"`) && strings.Contains(body, `"admin"`) && strings.Contains(body, `"password"`) {
hasLogin = true
}
if strings.Contains(body, `"add"`) && strings.Contains(body, `"site-a"`) {
hasAddSiteA = true
}
if strings.Contains(body, `"add"`) && strings.Contains(body, `"site-b"`) {
hasAddSiteB = true
}
}
assert.True(t, hasLogin, "expected login event to be called")
assert.True(t, hasAddSiteA, "expected site-a to be added")
assert.False(t, hasAddSiteB, "expected site-b NOT to be added (not in selected scope)")
}