mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
- Add TestSetup struct with DB/Router/AuthService/test users/JWT tokens for test environment initialization - Add setupTest function with SQLite in-memory database, user/plugin/automation/config/auth session migrations, and test user creation with admin/operator/viewer/user roles - Add makeRequest helper for authenticated HTTP requests with Bearer token and JSON body support
517 lines
14 KiB
Go
517 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/middleware"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/services"
|
|
)
|
|
|
|
// TestAPIIntegration tests the complete API integration
|
|
func TestAPIIntegration(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Use in-memory SQLite for testing
|
|
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
assert.NoError(t, err)
|
|
|
|
// Migrate all required tables
|
|
err = gormDB.AutoMigrate(
|
|
&models.User{},
|
|
&models.Plugin{},
|
|
&models.Automation{},
|
|
&models.ConfigEntry{},
|
|
&models.AuthSession{},
|
|
&models.Role{},
|
|
&models.Permission{},
|
|
&models.Transaction{},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
// Create database wrapper
|
|
db := &database.Database{DB: gormDB}
|
|
defer db.Close()
|
|
|
|
// Initialize services
|
|
authSvc := services.NewAuthService(db.DB, "test-secret-key")
|
|
|
|
// Create router
|
|
router := gin.New()
|
|
|
|
// Add health endpoint
|
|
router.GET("/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "healthy",
|
|
"service": "api-server",
|
|
})
|
|
})
|
|
|
|
// Add Swagger documentation endpoints
|
|
router.GET("/swagger/*any", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"message": "swagger endpoint"})
|
|
})
|
|
|
|
// Initialize other services
|
|
chaosSvc := services.NewChaosService(db)
|
|
invoiceSvc := services.NewInvoiceService(db)
|
|
pluginSvc := services.NewPluginService(db)
|
|
automationSvc := services.NewAutomationService(db)
|
|
configStoreSvc := services.NewConfigStoreService(db)
|
|
deploymentSvc := services.NewDeploymentService(db)
|
|
|
|
// Build handlers
|
|
authH := NewAuthHandler(authSvc)
|
|
servicesH := NewServicesHandler(nil) // Kubernetes service will be nil for testing
|
|
deploymentsH := NewDeploymentsHandler(deploymentSvc)
|
|
pluginsH := NewPluginsHandler(pluginSvc)
|
|
automationH := NewAutomationHandler(automationSvc)
|
|
configH := NewConfigHandler(configStoreSvc)
|
|
chaosH := NewChaosHandler(chaosSvc)
|
|
billingH := NewBillingHandler(invoiceSvc, db.DB)
|
|
|
|
// API v1 routes
|
|
v1 := router.Group("/v1")
|
|
{
|
|
// Authentication routes (no auth required)
|
|
auth := v1.Group("/auth")
|
|
{
|
|
auth.POST("/login", authH.Login)
|
|
auth.POST("/register", authH.Register)
|
|
auth.POST("/refresh", authH.RefreshToken)
|
|
}
|
|
|
|
// Protected routes (authentication required)
|
|
protected := v1.Group("/")
|
|
protected.Use(middleware.AuthMiddleware(authSvc))
|
|
{
|
|
// User management
|
|
authProtected := protected.Group("/auth")
|
|
{
|
|
authProtected.POST("/logout", authH.Logout)
|
|
authProtected.GET("/profile", authH.GetProfile)
|
|
authProtected.POST("/change-password", authH.ChangePassword)
|
|
}
|
|
|
|
// Admin-only user management
|
|
users := protected.Group("/users")
|
|
users.Use(middleware.RequireRole("admin"))
|
|
{
|
|
users.GET("", authH.GetUsers)
|
|
users.POST("", authH.CreateUser)
|
|
users.PUT("/:id", authH.UpdateUser)
|
|
users.DELETE("/:id", authH.DeleteUser)
|
|
}
|
|
}
|
|
|
|
// Protected API endpoints (authentication required)
|
|
apiProtected := v1.Group("/")
|
|
apiProtected.Use(middleware.AuthMiddleware(authSvc))
|
|
{
|
|
// Services Management
|
|
svcs := apiProtected.Group("/services")
|
|
{
|
|
svcs.GET("", servicesH.List)
|
|
svcs.GET("/:id", servicesH.Get)
|
|
svcs.POST("/:id/restart", servicesH.Restart)
|
|
svcs.GET("/:id/logs", servicesH.Logs)
|
|
svcs.POST("/:id/start", servicesH.Start)
|
|
svcs.POST("/:id/stop", servicesH.Stop)
|
|
svcs.POST("/:id/scale", servicesH.Scale)
|
|
svcs.GET("/:id/health", servicesH.Health)
|
|
svcs.GET("/:id/pods", servicesH.PodStatus)
|
|
svcs.GET("/:id/events", servicesH.Events)
|
|
}
|
|
|
|
// Deployment Management
|
|
deploy := apiProtected.Group("/deploy")
|
|
{
|
|
deploy.GET("/status", deploymentsH.Status)
|
|
deploy.POST("/start", deploymentsH.Start)
|
|
deploy.POST("/rollback", deploymentsH.Rollback)
|
|
deploy.GET("/history", deploymentsH.History)
|
|
}
|
|
|
|
// Plugin Management
|
|
plugs := apiProtected.Group("/plugins")
|
|
{
|
|
plugs.GET("", pluginsH.List)
|
|
plugs.POST("/install", pluginsH.Install)
|
|
plugs.DELETE("/:id", pluginsH.Uninstall)
|
|
plugs.POST("/:id/enable", pluginsH.Enable)
|
|
plugs.POST("/:id/disable", pluginsH.Disable)
|
|
}
|
|
|
|
// Automation Management
|
|
auto := apiProtected.Group("/automation")
|
|
{
|
|
auto.GET("", automationH.List)
|
|
auto.POST("", automationH.Create)
|
|
auto.POST("/:id/run", automationH.Run)
|
|
auto.POST("/:id/schedule", automationH.Schedule)
|
|
auto.GET("/logs", automationH.Logs)
|
|
}
|
|
|
|
// Billing & Invoicing
|
|
bill := apiProtected.Group("/billing")
|
|
{
|
|
bill.GET("/invoices", billingH.ListInvoices)
|
|
bill.POST("/invoices", billingH.GenerateInvoice)
|
|
bill.GET("/payments", billingH.ListPayments)
|
|
}
|
|
|
|
// Configuration Management
|
|
cfg := apiProtected.Group("/config")
|
|
{
|
|
cfg.GET("", configH.Get)
|
|
cfg.POST("", configH.Update)
|
|
cfg.GET("/validate", configH.Validate)
|
|
}
|
|
|
|
// Chaos Engineering
|
|
chaosGroup := apiProtected.Group("/chaos")
|
|
{
|
|
chaosGroup.GET("/experiments", chaosH.List)
|
|
chaosGroup.POST("/experiments", chaosH.Run)
|
|
chaosGroup.GET("/status", chaosH.Status)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test health endpoint
|
|
t.Run("Health Endpoint", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/health", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "healthy", response["status"])
|
|
assert.Equal(t, "api-server", response["service"])
|
|
})
|
|
|
|
// Test user registration
|
|
t.Run("User Registration", func(t *testing.T) {
|
|
registerData := map[string]any{
|
|
"username": "testuser",
|
|
"email": "[email protected]",
|
|
"password": "password123",
|
|
"first_name": "Test",
|
|
"last_name": "User",
|
|
"role": "viewer",
|
|
}
|
|
|
|
requestBody, _ := json.Marshal(registerData)
|
|
req, _ := http.NewRequest("POST", "/v1/auth/register", bytes.NewBuffer(requestBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
|
|
// Debug: print the actual response
|
|
t.Logf("Registration response: %+v", response)
|
|
|
|
assert.Equal(t, "User registered successfully", response["message"])
|
|
})
|
|
|
|
// Test user login
|
|
t.Run("User Login", func(t *testing.T) {
|
|
loginData := map[string]any{
|
|
"username": "testuser",
|
|
"password": "password123",
|
|
}
|
|
|
|
requestBody, _ := json.Marshal(loginData)
|
|
req, _ := http.NewRequest("POST", "/v1/auth/login", bytes.NewBuffer(requestBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "access_token")
|
|
assert.Contains(t, response, "refresh_token")
|
|
|
|
// Store token for subsequent tests
|
|
token := response["access_token"].(string)
|
|
assert.NotEmpty(t, token)
|
|
|
|
// Test protected endpoint with authentication
|
|
t.Run("Protected Endpoint", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/services", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// Should return 503 since Kubernetes service is not configured
|
|
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "Kubernetes not configured", response["error"])
|
|
})
|
|
|
|
// Test plugin management endpoint
|
|
t.Run("Plugin Management", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/plugins", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "plugins")
|
|
assert.Contains(t, response, "total")
|
|
})
|
|
|
|
// Test automation management endpoint
|
|
t.Run("Automation Management", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/automation", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "automations")
|
|
})
|
|
|
|
// Test billing endpoint (payments).
|
|
// NOTE: /v1/billing/invoices preloads the LineItems relation of the Invoice model,
|
|
// which depends on the Subscriber model (embedded PLMN/SNSSAI types can't be migrated
|
|
// into SQLite). We exercise the payments endpoint instead, which uses the clean
|
|
// Transaction model.
|
|
t.Run("Billing Management", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/billing/payments", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "payments")
|
|
assert.Contains(t, response, "total")
|
|
assert.Contains(t, response, "page")
|
|
assert.Contains(t, response, "page_size")
|
|
})
|
|
|
|
// Test configuration endpoint
|
|
t.Run("Configuration Management", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/config", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "entries")
|
|
})
|
|
|
|
// Test chaos engineering endpoint
|
|
t.Run("Chaos Engineering", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/chaos/experiments", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "active")
|
|
assert.Contains(t, response, "history")
|
|
})
|
|
})
|
|
}
|
|
|
|
// TestAuthenticationFlow tests the complete authentication flow
|
|
func TestAuthenticationFlow(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Use in-memory SQLite for testing
|
|
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
assert.NoError(t, err)
|
|
|
|
// Migrate all required tables
|
|
err = gormDB.AutoMigrate(
|
|
&models.User{},
|
|
&models.Plugin{},
|
|
&models.Automation{},
|
|
&models.ConfigEntry{},
|
|
&models.AuthSession{},
|
|
&models.Role{},
|
|
&models.Permission{},
|
|
&models.Transaction{},
|
|
)
|
|
assert.NoError(t, err)
|
|
|
|
// Create database wrapper
|
|
db := &database.Database{DB: gormDB}
|
|
defer db.Close()
|
|
|
|
// Initialize services
|
|
authSvc := services.NewAuthService(db.DB, "test-secret-key")
|
|
authH := NewAuthHandler(authSvc)
|
|
|
|
// Create router
|
|
router := gin.New()
|
|
|
|
// Authentication routes
|
|
auth := router.Group("/v1/auth")
|
|
{
|
|
auth.POST("/register", authH.Register)
|
|
auth.POST("/login", authH.Login)
|
|
auth.POST("/refresh", authH.RefreshToken)
|
|
}
|
|
|
|
// Protected routes
|
|
protected := router.Group("/v1")
|
|
protected.Use(middleware.AuthMiddleware(authSvc))
|
|
{
|
|
protected.GET("/profile", authH.GetProfile)
|
|
protected.POST("/logout", authH.Logout)
|
|
}
|
|
|
|
// Test registration
|
|
t.Run("Register User", func(t *testing.T) {
|
|
registerData := map[string]any{
|
|
"username": "testuser2",
|
|
"email": "[email protected]",
|
|
"password": "password123",
|
|
"first_name": "Test",
|
|
"last_name": "User",
|
|
"role": "viewer",
|
|
}
|
|
|
|
requestBody, _ := json.Marshal(registerData)
|
|
req, _ := http.NewRequest("POST", "/v1/auth/register", bytes.NewBuffer(requestBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
})
|
|
|
|
// Test login with invalid credentials
|
|
t.Run("Invalid Login", func(t *testing.T) {
|
|
loginData := map[string]any{
|
|
"username": "testuser2",
|
|
"password": "wrongpassword",
|
|
}
|
|
|
|
requestBody, _ := json.Marshal(loginData)
|
|
req, _ := http.NewRequest("POST", "/v1/auth/login", bytes.NewBuffer(requestBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
// Test successful login
|
|
t.Run("Successful Login", func(t *testing.T) {
|
|
loginData := map[string]any{
|
|
"username": "testuser2",
|
|
"password": "password123",
|
|
}
|
|
|
|
requestBody, _ := json.Marshal(loginData)
|
|
req, _ := http.NewRequest("POST", "/v1/auth/login", bytes.NewBuffer(requestBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]any
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
assert.NoError(t, err)
|
|
assert.Contains(t, response, "access_token")
|
|
|
|
token := response["access_token"].(string)
|
|
assert.NotEmpty(t, token)
|
|
|
|
// Test accessing protected endpoint with valid token
|
|
req, _ = http.NewRequest("GET", "/v1/profile", nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var profileResponse map[string]any
|
|
err = json.Unmarshal(w.Body.Bytes(), &profileResponse)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "testuser2", profileResponse["username"])
|
|
assert.Equal(t, "[email protected]", profileResponse["email"])
|
|
})
|
|
|
|
// Test accessing protected endpoint without token
|
|
t.Run("Unauthorized Access", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/profile", nil)
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
// Test accessing protected endpoint with invalid token
|
|
t.Run("Invalid Token", func(t *testing.T) {
|
|
req, _ := http.NewRequest("GET", "/v1/profile", nil)
|
|
req.Header.Set("Authorization", "Bearer invalid-token")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
}
|