Files
flvx/go-backend/tests/contract/speed_limit_contract_test.go
T

463 lines
14 KiB
Go

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
}