mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-12 04:06:37 +08:00
feat: add speed limit contract tests and refine limit/user UI
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/store/repo"
|
||||
)
|
||||
|
||||
// TestSpeedLimitWithoutTunnelContract tests that speed limits can be created without binding to a tunnel
|
||||
func TestSpeedLimitWithoutTunnelContract(t *testing.T) {
|
||||
secret := "contract-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)
|
||||
}
|
||||
|
||||
// Create a speed limit without tunnel binding
|
||||
t.Run("create speed limit without tunnel", func(t *testing.T) {
|
||||
body := `{"name":"test-limit-no-tunnel","speed":100,"status":1}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/create", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
// Verify the speed limit has null tunnelId
|
||||
t.Run("list speed limits shows null tunnelId", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/list", 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", out.Code)
|
||||
}
|
||||
|
||||
data, ok := out.Data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected data to be array, got %T", out.Data)
|
||||
}
|
||||
|
||||
// Find our speed limit
|
||||
var found bool
|
||||
for _, item := range data {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if m["name"] == "test-limit-no-tunnel" {
|
||||
found = true
|
||||
// tunnelId should be nil/not present for unbound speed limits
|
||||
if tunnelID, exists := m["tunnelId"]; exists && tunnelID != nil {
|
||||
t.Fatalf("expected tunnelId to be nil for unbound speed limit, got %v", tunnelID)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("speed limit 'test-limit-no-tunnel' not found in list")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSpeedLimitWithTunnelContract tests that speed limits can still be bound to tunnels
|
||||
func TestSpeedLimitWithTunnelContract(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, r := setupContractRouter(t, secret)
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
// First create a tunnel
|
||||
tunnelID := mustCreateSpeedLimitTunnel(t, r, "test-tunnel-for-limit")
|
||||
|
||||
// Create a speed limit with tunnel binding
|
||||
t.Run("create speed limit with tunnel", func(t *testing.T) {
|
||||
body := `{"name":"test-limit-with-tunnel","speed":200,"tunnelId":` + jsonInt(tunnelID) + `,"status":1}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/create", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
// Verify the speed limit has the tunnelId
|
||||
t.Run("list speed limits shows tunnelId", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/list", 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", out.Code)
|
||||
}
|
||||
|
||||
data, ok := out.Data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected data to be array, got %T", out.Data)
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, item := range data {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if m["name"] == "test-limit-with-tunnel" {
|
||||
found = true
|
||||
tunnelIDVal, exists := m["tunnelId"]
|
||||
if !exists || tunnelIDVal == nil {
|
||||
t.Fatal("expected tunnelId to be present for bound speed limit")
|
||||
}
|
||||
// Verify tunnelId matches
|
||||
if tunnelIDFloat, ok := tunnelIDVal.(float64); ok {
|
||||
if int64(tunnelIDFloat) != tunnelID {
|
||||
t.Fatalf("expected tunnelId %d, got %d", tunnelID, int64(tunnelIDFloat))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("speed limit 'test-limit-with-tunnel' not found in list")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSpeedLimitUpdateTunnelBindingContract tests updating speed limit tunnel binding
|
||||
func TestSpeedLimitUpdateTunnelBindingContract(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, r := setupContractRouter(t, secret)
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
// Create a tunnel
|
||||
tunnelID := mustCreateSpeedLimitTunnel(t, r, "test-tunnel-update")
|
||||
|
||||
// Create a speed limit without tunnel
|
||||
speedLimitID := mustCreateSpeedLimitRepo(t, r, "test-limit-update", 0)
|
||||
|
||||
// Update to bind to tunnel
|
||||
t.Run("update speed limit to bind tunnel", func(t *testing.T) {
|
||||
body := `{"id":` + jsonInt(speedLimitID) + `,"name":"test-limit-update","speed":150,"tunnelId":` + jsonInt(tunnelID) + `,"status":1}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/update", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
// Verify binding
|
||||
t.Run("verify tunnel binding after update", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/list", 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", out.Code)
|
||||
}
|
||||
|
||||
data, ok := out.Data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected data to be array, got %T", out.Data)
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if m["name"] == "test-limit-update" {
|
||||
tunnelIDVal, exists := m["tunnelId"]
|
||||
if !exists || tunnelIDVal == nil {
|
||||
t.Fatal("expected tunnelId to be present after update")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("speed limit 'test-limit-update' not found")
|
||||
})
|
||||
|
||||
// Update to unbind from tunnel (set tunnelId to null)
|
||||
t.Run("update speed limit to unbind tunnel", func(t *testing.T) {
|
||||
body := `{"id":` + jsonInt(speedLimitID) + `,"name":"test-limit-update","speed":150,"status":1}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/update", bytes.NewBufferString(body))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
// Verify unbinding
|
||||
t.Run("verify tunnel unbinding after update", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/list", 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", out.Code)
|
||||
}
|
||||
|
||||
data, ok := out.Data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected data to be array, got %T", out.Data)
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
m, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if m["name"] == "test-limit-update" {
|
||||
if tunnelIDVal, exists := m["tunnelId"]; exists && tunnelIDVal != nil {
|
||||
t.Fatalf("expected tunnelId to be nil after unbinding, got %v", tunnelIDVal)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("speed limit 'test-limit-update' not found")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSpeedLimitDatabaseNullableFields tests database-level nullable fields
|
||||
func TestSpeedLimitDatabaseNullableFields(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "speed-limit-null.db")
|
||||
r, err := repo.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = r.Close() })
|
||||
|
||||
// Create speed limit via repository
|
||||
t.Run("repository create speed limit without tunnel", func(t *testing.T) {
|
||||
id, err := r.CreateSpeedLimit("db-test-limit", 100, nil, "", 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSpeedLimit failed: %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Fatalf("expected valid id, got %d", id)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify TunnelID is null in database
|
||||
t.Run("verify null TunnelID in database", func(t *testing.T) {
|
||||
var tunnelID sql.NullInt64
|
||||
var tunnelName sql.NullString
|
||||
err := r.DB().Raw("SELECT tunnel_id, tunnel_name FROM speed_limit WHERE name = ?", "db-test-limit").Row().Scan(&tunnelID, &tunnelName)
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
if tunnelID.Valid {
|
||||
t.Fatalf("expected TunnelID to be NULL, got %d", tunnelID.Int64)
|
||||
}
|
||||
if tunnelName.Valid && tunnelName.String != "" {
|
||||
t.Fatalf("expected TunnelName to be NULL or empty, got %s", tunnelName.String)
|
||||
}
|
||||
})
|
||||
|
||||
// Create a tunnel for binding test
|
||||
tunnelID := mustCreateSpeedLimitTunnel(t, r, "db-test-tunnel")
|
||||
|
||||
// Create speed limit with tunnel
|
||||
t.Run("repository create speed limit with tunnel", func(t *testing.T) {
|
||||
id, err := r.CreateSpeedLimit("db-test-limit-with-tunnel", 200, &tunnelID, "db-test-tunnel", 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSpeedLimit failed: %v", err)
|
||||
}
|
||||
if id <= 0 {
|
||||
t.Fatalf("expected valid id, got %d", id)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify TunnelID is set
|
||||
t.Run("verify TunnelID is set in database", func(t *testing.T) {
|
||||
var dbTunnelID sql.NullInt64
|
||||
var dbTunnelName sql.NullString
|
||||
err := r.DB().Raw("SELECT tunnel_id, tunnel_name FROM speed_limit WHERE name = ?", "db-test-limit-with-tunnel").Row().Scan(&dbTunnelID, &dbTunnelName)
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
if !dbTunnelID.Valid {
|
||||
t.Fatal("expected TunnelID to be valid")
|
||||
}
|
||||
if dbTunnelID.Int64 != tunnelID {
|
||||
t.Fatalf("expected TunnelID %d, got %d", tunnelID, dbTunnelID.Int64)
|
||||
}
|
||||
if !dbTunnelName.Valid || dbTunnelName.String != "db-test-tunnel" {
|
||||
t.Fatalf("expected TunnelName 'db-test-tunnel', got %v", dbTunnelName.String)
|
||||
}
|
||||
})
|
||||
|
||||
// Test GetSpeedLimitTunnelID returns correct nullability
|
||||
t.Run("GetSpeedLimitTunnelID returns null for unbound limit", func(t *testing.T) {
|
||||
result := r.GetSpeedLimitTunnelID(1) // First speed limit (db-test-limit)
|
||||
if result.Valid {
|
||||
t.Fatalf("expected GetSpeedLimitTunnelID to return invalid/null, got valid with value %d", result.Int64)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSpeedLimitTunnelID returns value for bound limit", func(t *testing.T) {
|
||||
result := r.GetSpeedLimitTunnelID(2) // Second speed limit (db-test-limit-with-tunnel)
|
||||
if !result.Valid {
|
||||
t.Fatal("expected GetSpeedLimitTunnelID to return valid result for bound limit")
|
||||
}
|
||||
if result.Int64 != tunnelID {
|
||||
t.Fatalf("expected TunnelID %d, got %d", tunnelID, result.Int64)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSpeedLimitUpdateUnbindFromTunnel tests unbinding a speed limit from a tunnel
|
||||
func TestSpeedLimitUpdateUnbindFromTunnel(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "speed-limit-unbind.db")
|
||||
r, err := repo.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = r.Close() })
|
||||
|
||||
// Create tunnel
|
||||
tunnelID := mustCreateSpeedLimitTunnel(t, r, "unbind-test-tunnel")
|
||||
|
||||
// Create speed limit bound to tunnel
|
||||
speedLimitID, err := r.CreateSpeedLimit("unbind-test-limit", 300, &tunnelID, "unbind-test-tunnel", 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("create speed limit: %v", err)
|
||||
}
|
||||
|
||||
// Verify initial binding
|
||||
t.Run("verify initial binding", func(t *testing.T) {
|
||||
result := r.GetSpeedLimitTunnelID(speedLimitID)
|
||||
if !result.Valid {
|
||||
t.Fatal("expected initial binding to tunnel")
|
||||
}
|
||||
if result.Int64 != tunnelID {
|
||||
t.Fatalf("expected tunnel ID %d, got %d", tunnelID, result.Int64)
|
||||
}
|
||||
})
|
||||
|
||||
// Update to unbind
|
||||
t.Run("unbind speed limit from tunnel via UpdateSpeedLimit", func(t *testing.T) {
|
||||
err := r.UpdateSpeedLimit(speedLimitID, "unbind-test-limit", 300, nil, "", 1, time.Now().UnixMilli())
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateSpeedLimit failed: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify unbinding
|
||||
t.Run("verify unbinding after update", func(t *testing.T) {
|
||||
result := r.GetSpeedLimitTunnelID(speedLimitID)
|
||||
if result.Valid {
|
||||
t.Fatalf("expected GetSpeedLimitTunnelID to return invalid/null after unbind, got valid with value %d", result.Int64)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestSpeedLimitGetSpeed tests the GetSpeedLimitSpeed function
|
||||
func TestSpeedLimitGetSpeed(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "speed-limit-getspeed.db")
|
||||
r, err := repo.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = r.Close() })
|
||||
|
||||
// Create speed limit
|
||||
speedLimitID, err := r.CreateSpeedLimit("get-speed-test", 500, nil, "", 1, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("create speed limit: %v", err)
|
||||
}
|
||||
|
||||
// Test GetSpeedLimitSpeed
|
||||
t.Run("GetSpeedLimitSpeed returns correct speed", func(t *testing.T) {
|
||||
speed, err := r.GetSpeedLimitSpeed(speedLimitID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetSpeedLimitSpeed failed: %v", err)
|
||||
}
|
||||
if speed != 500 {
|
||||
t.Fatalf("expected speed 500, got %d", speed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSpeedLimitSpeed returns error for non-existent id", func(t *testing.T) {
|
||||
_, err := r.GetSpeedLimitSpeed(99999)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent speed limit ID")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func mustCreateSpeedLimitTunnel(t *testing.T, r *repo.Repository, name string) int64 {
|
||||
t.Helper()
|
||||
now := time.Now().UnixMilli()
|
||||
if err := r.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0)
|
||||
`, name, now, now).Error; err != nil {
|
||||
t.Fatalf("create tunnel failed: %v", err)
|
||||
}
|
||||
return mustLastInsertID(t, r, name)
|
||||
}
|
||||
|
||||
func mustCreateSpeedLimitRepo(t *testing.T, r *repo.Repository, name string, tunnelID int64) int64 {
|
||||
t.Helper()
|
||||
now := time.Now().UnixMilli()
|
||||
var tid *int64
|
||||
if tunnelID > 0 {
|
||||
tid = &tunnelID
|
||||
}
|
||||
id, err := r.CreateSpeedLimit(name, 100, tid, "", now, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("create speed limit failed: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
Reference in New Issue
Block a user