mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
1452 lines
40 KiB
Go
1452 lines
40 KiB
Go
package contract_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"go-backend/internal/auth"
|
|
"go-backend/internal/http/response"
|
|
"go-backend/internal/store/model"
|
|
)
|
|
|
|
func TestNodeMetricsEndpoints(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
seedContractUser(t, repo, 2, "normal_user", 1, 1)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
t.Run("insert and query node metrics", func(t *testing.T) {
|
|
metric := &model.NodeMetric{
|
|
NodeID: 1,
|
|
Timestamp: now - 1000,
|
|
CPUUsage: 45.5,
|
|
MemUsage: 60.2,
|
|
DiskUsage: 30.1,
|
|
NetInBytes: 1024000,
|
|
NetOutBytes: 2048000,
|
|
NetInSpeed: 51200,
|
|
NetOutSpeed: 102400,
|
|
Load1: 1.5,
|
|
Load5: 1.2,
|
|
Load15: 0.9,
|
|
TCPConns: 100,
|
|
UDPConns: 50,
|
|
Uptime: 86400,
|
|
}
|
|
if err := repo.InsertNodeMetric(metric); err != nil {
|
|
t.Fatalf("insert node metric: %v", err)
|
|
}
|
|
|
|
metric2 := &model.NodeMetric{
|
|
NodeID: 1,
|
|
Timestamp: now,
|
|
CPUUsage: 50.0,
|
|
MemUsage: 65.0,
|
|
DiskUsage: 32.0,
|
|
NetInBytes: 2048000,
|
|
NetOutBytes: 4096000,
|
|
NetInSpeed: 102400,
|
|
NetOutSpeed: 204800,
|
|
Load1: 2.0,
|
|
Load5: 1.5,
|
|
Load15: 1.0,
|
|
TCPConns: 150,
|
|
UDPConns: 75,
|
|
Uptime: 172800,
|
|
}
|
|
if err := repo.InsertNodeMetric(metric2); err != nil {
|
|
t.Fatalf("insert node metric 2: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes/1/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
metrics, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(metrics) != 2 {
|
|
t.Fatalf("expected 2 metrics, got %d", len(metrics))
|
|
}
|
|
})
|
|
|
|
t.Run("get latest node metric", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes/1/metrics/latest", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
metric, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected object, got %T", out.Data)
|
|
}
|
|
if cpu, _ := metric["cpuUsage"].(float64); cpu != 50.0 {
|
|
t.Fatalf("expected cpuUsage 50.0, got %v", cpu)
|
|
}
|
|
})
|
|
|
|
t.Run("get metrics for non-existent node returns empty array", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes/999/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
metrics, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(metrics) != 0 {
|
|
t.Fatalf("expected 0 metrics for non-existent node, got %d", len(metrics))
|
|
}
|
|
})
|
|
|
|
t.Run("get latest metric for non-existent node returns nil", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes/999/metrics/latest", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
if out.Data != nil {
|
|
t.Fatalf("expected nil data for non-existent node, got %v", out.Data)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid node id returns error", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes/invalid/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for invalid node id")
|
|
}
|
|
})
|
|
|
|
t.Run("query with time range", func(t *testing.T) {
|
|
start := now - 2000
|
|
end := now - 500
|
|
path := "/api/v1/monitor/nodes/1/metrics?start=" + jsonNumber(start) + "&end=" + jsonNumber(end)
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
metrics, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(metrics) != 1 {
|
|
t.Fatalf("expected 1 metric in time range, got %d", len(metrics))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTunnelMetricsEndpoints(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
t.Run("insert and query tunnel metrics", func(t *testing.T) {
|
|
metric := &model.TunnelMetric{
|
|
TunnelID: 1,
|
|
NodeID: 1,
|
|
Timestamp: now - 1000,
|
|
BytesIn: 1024000,
|
|
BytesOut: 2048000,
|
|
Connections: 10,
|
|
Errors: 0,
|
|
AvgLatencyMs: 15.5,
|
|
}
|
|
if err := repo.InsertTunnelMetric(metric); err != nil {
|
|
t.Fatalf("insert tunnel metric: %v", err)
|
|
}
|
|
|
|
metric2 := &model.TunnelMetric{
|
|
TunnelID: 1,
|
|
NodeID: 1,
|
|
Timestamp: now,
|
|
BytesIn: 2048000,
|
|
BytesOut: 4096000,
|
|
Connections: 20,
|
|
Errors: 1,
|
|
AvgLatencyMs: 20.0,
|
|
}
|
|
if err := repo.InsertTunnelMetric(metric2); err != nil {
|
|
t.Fatalf("insert tunnel metric 2: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/tunnels/1/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
metrics, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(metrics) != 2 {
|
|
t.Fatalf("expected 2 metrics, got %d", len(metrics))
|
|
}
|
|
})
|
|
|
|
t.Run("get metrics for non-existent tunnel returns empty array", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/tunnels/999/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
metrics, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(metrics) != 0 {
|
|
t.Fatalf("expected 0 metrics for non-existent tunnel, got %d", len(metrics))
|
|
}
|
|
})
|
|
|
|
t.Run("invalid tunnel id returns error", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/tunnels/invalid/metrics", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for invalid tunnel id")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestServiceMonitorCRUD(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
// Seed a node for node-executed monitors.
|
|
now := time.Now().UnixMilli()
|
|
n := &model.Node{
|
|
Name: "node-1",
|
|
Secret: "node-secret",
|
|
ServerIP: "127.0.0.1",
|
|
Port: "10000-10010",
|
|
TCPListenAddr: "[::]",
|
|
UDPListenAddr: "[::]",
|
|
CreatedTime: now,
|
|
Status: 0,
|
|
}
|
|
if err := repo.DB().Create(n).Error; err != nil {
|
|
t.Fatalf("seed node: %v", err)
|
|
}
|
|
|
|
t.Run("create service monitor - TCP", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"name": "DNS Monitor",
|
|
"type": "tcp",
|
|
"target": "8.8.8.8:53",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"nodeId": 0,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
monitor, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected object, got %T", out.Data)
|
|
}
|
|
if name, _ := monitor["name"].(string); name != "DNS Monitor" {
|
|
t.Fatalf("expected name 'DNS Monitor', got %v", name)
|
|
}
|
|
if monitorType, _ := monitor["type"].(string); monitorType != "tcp" {
|
|
t.Fatalf("expected type 'tcp', got %v", monitorType)
|
|
}
|
|
})
|
|
|
|
t.Run("create service monitor - ICMP", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"name": "Ping Monitor",
|
|
"type": "icmp",
|
|
"target": "8.8.8.8",
|
|
"intervalSec": 30,
|
|
"timeoutSec": 10,
|
|
"nodeId": n.ID,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
})
|
|
|
|
t.Run("list service monitors", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
monitors, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(monitors) != 2 {
|
|
t.Fatalf("expected 2 monitors, got %d", len(monitors))
|
|
}
|
|
})
|
|
|
|
t.Run("create monitor with invalid type returns error", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"name": "Invalid Monitor",
|
|
"type": "http",
|
|
"target": "https://example.com",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for invalid monitor type")
|
|
}
|
|
})
|
|
|
|
t.Run("create monitor with empty name returns error", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"name": "",
|
|
"type": "tcp",
|
|
"target": "8.8.8.8:53",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for empty name")
|
|
}
|
|
})
|
|
|
|
t.Run("create monitor with empty target returns error", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"name": "No Target",
|
|
"type": "tcp",
|
|
"target": "",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for empty target")
|
|
}
|
|
})
|
|
|
|
var monitorID int64
|
|
t.Run("update service monitor", func(t *testing.T) {
|
|
monitors, _ := repo.ListServiceMonitors()
|
|
if len(monitors) == 0 {
|
|
t.Fatalf("no monitors to update")
|
|
}
|
|
monitorID = monitors[0].ID
|
|
|
|
payload := map[string]interface{}{
|
|
"id": monitorID,
|
|
"name": "Updated DNS Monitor",
|
|
"type": "tcp",
|
|
"target": "1.1.1.1:53",
|
|
"intervalSec": 120,
|
|
"timeoutSec": 10,
|
|
"nodeId": 0,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/update", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
updated, _ := repo.GetServiceMonitor(monitorID)
|
|
if updated.Name != "Updated DNS Monitor" {
|
|
t.Fatalf("expected name 'Updated DNS Monitor', got %s", updated.Name)
|
|
}
|
|
if updated.IntervalSec != 120 {
|
|
t.Fatalf("expected interval 120, got %d", updated.IntervalSec)
|
|
}
|
|
})
|
|
|
|
t.Run("update non-existent monitor returns error", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"id": 99999,
|
|
"name": "Non-existent",
|
|
"type": "tcp",
|
|
"target": "1.1.1.1:53",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"enabled": 1,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/update", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code == 0 {
|
|
t.Fatalf("expected error for non-existent monitor")
|
|
}
|
|
})
|
|
|
|
t.Run("delete service monitor", func(t *testing.T) {
|
|
monitors, _ := repo.ListServiceMonitors()
|
|
if len(monitors) < 2 {
|
|
t.Fatalf("need at least 2 monitors to test delete")
|
|
}
|
|
deleteID := monitors[1].ID
|
|
|
|
// Seed history so we can verify deletion cleanup.
|
|
if err := repo.InsertServiceMonitorResult(&model.ServiceMonitorResult{
|
|
MonitorID: deleteID,
|
|
NodeID: 0,
|
|
Timestamp: now,
|
|
Success: 1,
|
|
LatencyMs: 1,
|
|
StatusCode: 0,
|
|
ErrorMessage: "",
|
|
}); err != nil {
|
|
t.Fatalf("seed monitor result: %v", err)
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"id": deleteID,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/delete", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
remaining, _ := repo.ListServiceMonitors()
|
|
if len(remaining) != 1 {
|
|
t.Fatalf("expected 1 remaining monitor, got %d", len(remaining))
|
|
}
|
|
results, err := repo.GetServiceMonitorResults(deleteID, 10)
|
|
if err != nil {
|
|
t.Fatalf("get deleted monitor results: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Fatalf("expected 0 results for deleted monitor, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
t.Run("delete non-existent monitor returns success", func(t *testing.T) {
|
|
payload := map[string]interface{}{
|
|
"id": 99999,
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/services/delete", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 for delete non-existent, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestServiceMonitorResults(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
monitor := &model.ServiceMonitor{
|
|
Name: "Test Monitor",
|
|
Type: "tcp",
|
|
Target: "8.8.8.8:53",
|
|
IntervalSec: 60,
|
|
TimeoutSec: 5,
|
|
NodeID: 0,
|
|
Enabled: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
}
|
|
if err := repo.CreateServiceMonitor(monitor); err != nil {
|
|
t.Fatalf("create monitor: %v", err)
|
|
}
|
|
|
|
t.Run("insert and query monitor results", func(t *testing.T) {
|
|
for i := 0; i < 5; i++ {
|
|
result := &model.ServiceMonitorResult{
|
|
MonitorID: monitor.ID,
|
|
NodeID: 0,
|
|
Timestamp: now - int64(i*60000),
|
|
Success: 1,
|
|
LatencyMs: float64(10 + i),
|
|
StatusCode: 0,
|
|
ErrorMessage: "",
|
|
}
|
|
if err := repo.InsertServiceMonitorResult(result); err != nil {
|
|
t.Fatalf("insert result %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services/"+jsonNumber(monitor.ID)+"/results", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
results, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(results) != 5 {
|
|
t.Fatalf("expected 5 results, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
t.Run("query results with limit", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services/"+jsonNumber(monitor.ID)+"/results?limit=3", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
results, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(results) != 3 {
|
|
t.Fatalf("expected 3 results with limit, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
t.Run("query results for non-existent monitor returns empty", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services/99999/results", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
results, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Fatalf("expected 0 results for non-existent monitor, got %d", len(results))
|
|
}
|
|
})
|
|
|
|
t.Run("result with error message", func(t *testing.T) {
|
|
result := &model.ServiceMonitorResult{
|
|
MonitorID: monitor.ID,
|
|
NodeID: 0,
|
|
Timestamp: now,
|
|
Success: 0,
|
|
LatencyMs: 0,
|
|
StatusCode: 0,
|
|
ErrorMessage: "connection refused",
|
|
}
|
|
if err := repo.InsertServiceMonitorResult(result); err != nil {
|
|
t.Fatalf("insert failed result: %v", err)
|
|
}
|
|
|
|
results, _ := repo.GetServiceMonitorResults(monitor.ID, 100)
|
|
found := false
|
|
for _, r := range results {
|
|
if r.ErrorMessage == "connection refused" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected to find result with error message")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestServiceMonitorLatestResultsEndpoint(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
m1 := &model.ServiceMonitor{
|
|
Name: "m1",
|
|
Type: "tcp",
|
|
Target: "127.0.0.1:1",
|
|
IntervalSec: 60,
|
|
TimeoutSec: 1,
|
|
NodeID: 0,
|
|
Enabled: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
}
|
|
if err := repo.CreateServiceMonitor(m1); err != nil {
|
|
t.Fatalf("create monitor 1: %v", err)
|
|
}
|
|
m2 := &model.ServiceMonitor{
|
|
Name: "m2",
|
|
Type: "tcp",
|
|
Target: "127.0.0.1:2",
|
|
IntervalSec: 60,
|
|
TimeoutSec: 1,
|
|
NodeID: 0,
|
|
Enabled: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
}
|
|
if err := repo.CreateServiceMonitor(m2); err != nil {
|
|
t.Fatalf("create monitor 2: %v", err)
|
|
}
|
|
|
|
if err := repo.InsertServiceMonitorResult(&model.ServiceMonitorResult{
|
|
MonitorID: m1.ID,
|
|
NodeID: 0,
|
|
Timestamp: now - 60_000,
|
|
Success: 1,
|
|
LatencyMs: 10,
|
|
}); err != nil {
|
|
t.Fatalf("insert m1 old result: %v", err)
|
|
}
|
|
if err := repo.InsertServiceMonitorResult(&model.ServiceMonitorResult{
|
|
MonitorID: m1.ID,
|
|
NodeID: 0,
|
|
Timestamp: now,
|
|
Success: 0,
|
|
LatencyMs: 20,
|
|
}); err != nil {
|
|
t.Fatalf("insert m1 latest result: %v", err)
|
|
}
|
|
if err := repo.InsertServiceMonitorResult(&model.ServiceMonitorResult{
|
|
MonitorID: m2.ID,
|
|
NodeID: 0,
|
|
Timestamp: now - 30_000,
|
|
Success: 1,
|
|
LatencyMs: 5,
|
|
}); err != nil {
|
|
t.Fatalf("insert m2 result: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services/latest-results", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
rows, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 latest-result rows, got %d", len(rows))
|
|
}
|
|
|
|
seen := make(map[int64]int64, len(rows))
|
|
for _, raw := range rows {
|
|
m, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
mid, _ := m["monitorId"].(float64)
|
|
ts, _ := m["timestamp"].(float64)
|
|
if mid <= 0 {
|
|
continue
|
|
}
|
|
seen[int64(mid)] = int64(ts)
|
|
}
|
|
if len(seen) != 2 {
|
|
t.Fatalf("expected 2 unique monitorIds, got %d", len(seen))
|
|
}
|
|
if seen[m1.ID] != now {
|
|
t.Fatalf("expected m1 latest timestamp %d, got %d", now, seen[m1.ID])
|
|
}
|
|
if seen[m2.ID] != now-30_000 {
|
|
t.Fatalf("expected m2 latest timestamp %d, got %d", now-30_000, seen[m2.ID])
|
|
}
|
|
}
|
|
|
|
func TestServiceMonitorLimitsEndpoint(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, _ := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/services/limits", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
limits, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected object, got %T", out.Data)
|
|
}
|
|
for _, key := range []string{
|
|
"checkerScanIntervalSec",
|
|
"minIntervalSec",
|
|
"defaultIntervalSec",
|
|
"minTimeoutSec",
|
|
"defaultTimeoutSec",
|
|
"maxTimeoutSec",
|
|
} {
|
|
if _, ok := limits[key]; !ok {
|
|
t.Fatalf("expected %q in limits", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMonitorNodeAndTunnelListEndpoints(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
n1 := &model.Node{
|
|
Name: "node-a",
|
|
Secret: "node-a-secret",
|
|
ServerIP: "127.0.0.1",
|
|
Port: "10000-10010",
|
|
TCPListenAddr: "[::]",
|
|
UDPListenAddr: "[::]",
|
|
Inx: 2,
|
|
CreatedTime: now,
|
|
Status: 1,
|
|
}
|
|
if err := repo.DB().Create(n1).Error; err != nil {
|
|
t.Fatalf("seed node 1: %v", err)
|
|
}
|
|
n2 := &model.Node{
|
|
Name: "node-b",
|
|
Secret: "node-b-secret",
|
|
ServerIP: "127.0.0.1",
|
|
Port: "11000-11010",
|
|
TCPListenAddr: "[::]",
|
|
UDPListenAddr: "[::]",
|
|
Inx: 1,
|
|
CreatedTime: now,
|
|
Status: 0,
|
|
}
|
|
if err := repo.DB().Create(n2).Error; err != nil {
|
|
t.Fatalf("seed node 2: %v", err)
|
|
}
|
|
|
|
t1 := &model.Tunnel{
|
|
Name: "tunnel-a",
|
|
TrafficRatio: 1.0,
|
|
Type: 1,
|
|
Protocol: "tls",
|
|
Flow: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
Status: 1,
|
|
Inx: 2,
|
|
}
|
|
if err := repo.DB().Create(t1).Error; err != nil {
|
|
t.Fatalf("seed tunnel 1: %v", err)
|
|
}
|
|
t2 := &model.Tunnel{
|
|
Name: "tunnel-b",
|
|
TrafficRatio: 1.0,
|
|
Type: 1,
|
|
Protocol: "tls",
|
|
Flow: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
Status: 0,
|
|
Inx: 1,
|
|
}
|
|
if err := repo.DB().Create(t2).Error; err != nil {
|
|
t.Fatalf("seed tunnel 2: %v", err)
|
|
}
|
|
|
|
// Nodes
|
|
{
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/nodes", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
rows, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 nodes, got %d", len(rows))
|
|
}
|
|
first, _ := rows[0].(map[string]interface{})
|
|
if name, _ := first["name"].(string); name != "node-b" {
|
|
t.Fatalf("expected node order by inx (node-b first), got %q", name)
|
|
}
|
|
}
|
|
|
|
// Tunnels
|
|
{
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/tunnels", nil)
|
|
req.Header.Set("Authorization", adminToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
rows, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array, got %T", out.Data)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("expected 2 tunnels, got %d", len(rows))
|
|
}
|
|
first, _ := rows[0].(map[string]interface{})
|
|
if name, _ := first["name"].(string); name != "tunnel-b" {
|
|
t.Fatalf("expected tunnel order by inx (tunnel-b first), got %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMonitorPermissionAdminEndpoints(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
u := &model.User{
|
|
User: "test-user",
|
|
Pwd: "pwd",
|
|
RoleID: 1,
|
|
ExpTime: now + 3600_000,
|
|
Flow: 0,
|
|
FlowResetTime: now,
|
|
Num: 0,
|
|
CreatedTime: now,
|
|
Status: 1,
|
|
}
|
|
if err := repo.DB().Create(u).Error; err != nil {
|
|
t.Fatalf("seed user: %v", err)
|
|
}
|
|
|
|
assignBody, _ := json.Marshal(map[string]interface{}{"userId": u.ID})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/permission/assign", bytes.NewReader(assignBody))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode assign response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 on assign, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/permission/list", nil)
|
|
listReq.Header.Set("Authorization", adminToken)
|
|
listRes := httptest.NewRecorder()
|
|
router.ServeHTTP(listRes, listReq)
|
|
|
|
if err := json.NewDecoder(listRes.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode list response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 on list, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
rows, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array on list, got %T", out.Data)
|
|
}
|
|
found := false
|
|
for _, raw := range rows {
|
|
m, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
uid, _ := m["userId"].(float64)
|
|
if int64(uid) == u.ID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("expected assigned permission to appear in list")
|
|
}
|
|
|
|
removeBody, _ := json.Marshal(map[string]interface{}{"userId": u.ID})
|
|
remReq := httptest.NewRequest(http.MethodPost, "/api/v1/monitor/permission/remove", bytes.NewReader(removeBody))
|
|
remReq.Header.Set("Authorization", adminToken)
|
|
remReq.Header.Set("Content-Type", "application/json")
|
|
remRes := httptest.NewRecorder()
|
|
router.ServeHTTP(remRes, remReq)
|
|
|
|
if err := json.NewDecoder(remRes.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode remove response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 on remove, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
|
|
listReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/permission/list", nil)
|
|
listReq2.Header.Set("Authorization", adminToken)
|
|
listRes2 := httptest.NewRecorder()
|
|
router.ServeHTTP(listRes2, listReq2)
|
|
|
|
if err := json.NewDecoder(listRes2.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode list response after remove: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 on list after remove, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
rows2, ok := out.Data.([]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected array on list after remove, got %T", out.Data)
|
|
}
|
|
for _, raw := range rows2 {
|
|
m, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
uid, _ := m["userId"].(float64)
|
|
if int64(uid) == u.ID {
|
|
t.Fatalf("expected removed permission to be absent")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMetricBatchInsert(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
_, repo := setupContractRouter(t, secret)
|
|
|
|
t.Run("batch insert node metrics", func(t *testing.T) {
|
|
now := time.Now().UnixMilli()
|
|
var metrics []*model.NodeMetric
|
|
for i := 0; i < 10; i++ {
|
|
metrics = append(metrics, &model.NodeMetric{
|
|
NodeID: 1,
|
|
Timestamp: now - int64(i*1000),
|
|
CPUUsage: float64(40 + i),
|
|
MemUsage: float64(50 + i),
|
|
DiskUsage: 30.0,
|
|
NetInBytes: int64(1000 * (i + 1)),
|
|
NetOutBytes: int64(2000 * (i + 1)),
|
|
NetInSpeed: 100,
|
|
NetOutSpeed: 200,
|
|
Load1: 1.0,
|
|
Load5: 0.8,
|
|
Load15: 0.6,
|
|
TCPConns: 100,
|
|
UDPConns: 50,
|
|
})
|
|
}
|
|
|
|
if err := repo.InsertNodeMetricBatch(metrics); err != nil {
|
|
t.Fatalf("batch insert: %v", err)
|
|
}
|
|
|
|
retrieved, err := repo.GetNodeMetrics(1, now-10000, now+1000)
|
|
if err != nil {
|
|
t.Fatalf("get metrics: %v", err)
|
|
}
|
|
if len(retrieved) != 10 {
|
|
t.Fatalf("expected 10 metrics, got %d", len(retrieved))
|
|
}
|
|
})
|
|
|
|
t.Run("batch insert tunnel metrics", func(t *testing.T) {
|
|
now := time.Now().UnixMilli()
|
|
var metrics []*model.TunnelMetric
|
|
for i := 0; i < 5; i++ {
|
|
metrics = append(metrics, &model.TunnelMetric{
|
|
TunnelID: 1,
|
|
NodeID: 1,
|
|
Timestamp: now - int64(i*1000),
|
|
BytesIn: int64(1000 * (i + 1)),
|
|
BytesOut: int64(2000 * (i + 1)),
|
|
Connections: int64(i + 1),
|
|
Errors: 0,
|
|
AvgLatencyMs: float64(10 + i),
|
|
})
|
|
}
|
|
|
|
if err := repo.InsertTunnelMetricBatch(metrics); err != nil {
|
|
t.Fatalf("batch insert: %v", err)
|
|
}
|
|
|
|
retrieved, err := repo.GetTunnelMetrics(1, 0, now+1000)
|
|
if err != nil {
|
|
t.Fatalf("get metrics: %v", err)
|
|
}
|
|
if len(retrieved) != 5 {
|
|
t.Fatalf("expected 5 metrics, got %d", len(retrieved))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMetricPruning(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
_, repo := setupContractRouter(t, secret)
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
oldMetric := &model.NodeMetric{
|
|
NodeID: 1,
|
|
Timestamp: now - 8*24*60*60*1000,
|
|
CPUUsage: 50,
|
|
MemUsage: 60,
|
|
DiskUsage: 30,
|
|
Load1: 1.0,
|
|
Load5: 0.8,
|
|
Load15: 0.6,
|
|
TCPConns: 100,
|
|
UDPConns: 50,
|
|
}
|
|
if err := repo.InsertNodeMetric(oldMetric); err != nil {
|
|
t.Fatalf("insert old metric: %v", err)
|
|
}
|
|
|
|
newMetric := &model.NodeMetric{
|
|
NodeID: 1,
|
|
Timestamp: now,
|
|
CPUUsage: 55,
|
|
MemUsage: 65,
|
|
DiskUsage: 32,
|
|
Load1: 1.2,
|
|
Load5: 0.9,
|
|
Load15: 0.7,
|
|
TCPConns: 120,
|
|
UDPConns: 60,
|
|
}
|
|
if err := repo.InsertNodeMetric(newMetric); err != nil {
|
|
t.Fatalf("insert new metric: %v", err)
|
|
}
|
|
|
|
cutoff := now - 7*24*60*60*1000
|
|
if err := repo.PruneNodeMetrics(cutoff); err != nil {
|
|
t.Fatalf("prune metrics: %v", err)
|
|
}
|
|
|
|
retrieved, err := repo.GetNodeMetrics(1, 0, now+1000)
|
|
if err != nil {
|
|
t.Fatalf("get metrics: %v", err)
|
|
}
|
|
if len(retrieved) != 1 {
|
|
t.Fatalf("expected 1 metric after pruning, got %d", len(retrieved))
|
|
}
|
|
}
|
|
|
|
func TestMonitoringAuthRequired(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, _ := setupContractRouter(t, secret)
|
|
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
}{
|
|
{"node metrics", http.MethodGet, "/api/v1/monitor/nodes/1/metrics"},
|
|
{"node metrics latest", http.MethodGet, "/api/v1/monitor/nodes/1/metrics/latest"},
|
|
{"tunnel metrics", http.MethodGet, "/api/v1/monitor/tunnels/1/metrics"},
|
|
{"service list", http.MethodGet, "/api/v1/monitor/services"},
|
|
{"service results", http.MethodGet, "/api/v1/monitor/services/1/results"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name+" requires auth", func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 401 {
|
|
t.Fatalf("expected 401 for missing auth, got %d", out.Code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMonitorAccessEndpoint(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
seedContractUser(t, repo, 2, "normal_user", 1, 1)
|
|
|
|
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate admin token: %v", err)
|
|
}
|
|
userToken, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate user token: %v", err)
|
|
}
|
|
|
|
assertAllowed := func(t *testing.T, token string, want bool) {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/monitor/access", nil)
|
|
req.Header.Set("Authorization", token)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0, got %d: %s", out.Code, out.Msg)
|
|
}
|
|
data, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected object, got %T", out.Data)
|
|
}
|
|
allowed, _ := data["allowed"].(bool)
|
|
if allowed != want {
|
|
t.Fatalf("expected allowed=%v, got %v", want, allowed)
|
|
}
|
|
}
|
|
|
|
t.Run("admin is allowed", func(t *testing.T) {
|
|
assertAllowed(t, adminToken, true)
|
|
})
|
|
|
|
t.Run("non-admin without grant is denied", func(t *testing.T) {
|
|
assertAllowed(t, userToken, false)
|
|
})
|
|
|
|
now := time.Now().UnixMilli()
|
|
if err := repo.InsertMonitorPermission(2, now); err != nil {
|
|
t.Fatalf("insert monitor permission: %v", err)
|
|
}
|
|
|
|
t.Run("non-admin with grant is allowed", func(t *testing.T) {
|
|
assertAllowed(t, userToken, true)
|
|
})
|
|
}
|
|
|
|
func TestMonitoringPermissionRequired(t *testing.T) {
|
|
secret := "monitoring-jwt-secret"
|
|
router, repo := setupContractRouter(t, secret)
|
|
seedContractUser(t, repo, 2, "normal_user", 1, 1)
|
|
|
|
userToken, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
|
if err != nil {
|
|
t.Fatalf("generate user token: %v", err)
|
|
}
|
|
|
|
createBody, _ := json.Marshal(map[string]interface{}{
|
|
"name": "NonAdmin Monitor",
|
|
"type": "tcp",
|
|
"target": "127.0.0.1:1",
|
|
"intervalSec": 60,
|
|
"timeoutSec": 5,
|
|
"nodeId": 0,
|
|
"enabled": 1,
|
|
})
|
|
|
|
forbidden := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body []byte
|
|
}{
|
|
{"service list", http.MethodGet, "/api/v1/monitor/services", nil},
|
|
{"service create", http.MethodPost, "/api/v1/monitor/services/create", createBody},
|
|
{"node metrics", http.MethodGet, "/api/v1/monitor/nodes/1/metrics", nil},
|
|
}
|
|
|
|
for _, tc := range forbidden {
|
|
t.Run(tc.name+" forbidden without grant", func(t *testing.T) {
|
|
var req *http.Request
|
|
if tc.body != nil {
|
|
req = httptest.NewRequest(tc.method, tc.path, bytes.NewReader(tc.body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(tc.method, tc.path, nil)
|
|
}
|
|
req.Header.Set("Authorization", userToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 403 {
|
|
t.Fatalf("expected 403 without grant, got %d msg=%q", out.Code, out.Msg)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Grant monitor permission and verify access.
|
|
now := time.Now().UnixMilli()
|
|
if err := repo.InsertMonitorPermission(2, now); err != nil {
|
|
t.Fatalf("insert monitor permission: %v", err)
|
|
}
|
|
|
|
allowed := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
body []byte
|
|
}{
|
|
{"service list", http.MethodGet, "/api/v1/monitor/services", nil},
|
|
{"service create", http.MethodPost, "/api/v1/monitor/services/create", createBody},
|
|
}
|
|
|
|
for _, tc := range allowed {
|
|
t.Run(tc.name+" allowed with grant", func(t *testing.T) {
|
|
var req *http.Request
|
|
if tc.body != nil {
|
|
req = httptest.NewRequest(tc.method, tc.path, bytes.NewReader(tc.body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req = httptest.NewRequest(tc.method, tc.path, nil)
|
|
}
|
|
req.Header.Set("Authorization", userToken)
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 with grant, got %d msg=%q", out.Code, out.Msg)
|
|
}
|
|
})
|
|
}
|
|
}
|