mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
369 lines
11 KiB
Go
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)")
|
|
}
|