mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-22 04:34:27 +00:00
- Mark unused context and filter parameters with underscore in getCarrierMetrics, getGeoMetrics, and getPerformanceStats - Remove unused RatePlanCurrencyIntegrator service from currency/services/rateplan_core.go - Simplify ML model weight comparison in selection_test_ml.go by using literal values instead of originalWeights variable - Add comprehensive subscriber_journey_test.go with full lifecycle
441 lines
12 KiB
Go
441 lines
12 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// APIIntegrationTestSuite tests API integration
|
|
type APIIntegrationTestSuite struct {
|
|
suite.Suite
|
|
db *gorm.DB
|
|
router *gin.Engine
|
|
logger *logrus.Logger
|
|
}
|
|
|
|
// SetupSuite sets up the test suite
|
|
func (suite *APIIntegrationTestSuite) SetupSuite() {
|
|
// Setup in-memory database for testing
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(suite.T(), err)
|
|
|
|
// Auto-migrate tables
|
|
err = db.AutoMigrate(
|
|
&repository.RatePlanUsage{},
|
|
&repository.RatePlanSubscription{},
|
|
)
|
|
require.NoError(suite.T(), err)
|
|
|
|
suite.db = db
|
|
suite.logger = logrus.New()
|
|
suite.logger.SetLevel(logrus.ErrorLevel)
|
|
|
|
// Setup router
|
|
gin.SetMode(gin.TestMode)
|
|
suite.router = gin.New()
|
|
suite.setupRoutes()
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) setupRoutes() {
|
|
api := suite.router.Group("/api/v1")
|
|
|
|
// Rate plan usage routes
|
|
usage := api.Group("/usage")
|
|
{
|
|
usage.POST("", suite.createUsage)
|
|
usage.GET("/:id", suite.getUsage)
|
|
usage.GET("", suite.listUsage)
|
|
}
|
|
|
|
// Subscription routes
|
|
subscriptions := api.Group("/subscriptions")
|
|
{
|
|
subscriptions.POST("", suite.createSubscription)
|
|
subscriptions.GET("/:id", suite.getSubscription)
|
|
subscriptions.PUT("/:id/cancel", suite.cancelSubscription)
|
|
}
|
|
|
|
// Health check
|
|
api.GET("/health", suite.healthCheck)
|
|
}
|
|
|
|
// TestRatePlanUsageLifecycle tests usage tracking
|
|
func (suite *APIIntegrationTestSuite) TestRatePlanUsageLifecycle() {
|
|
// 1. Create a rate plan usage record
|
|
usageReq := map[string]interface{}{
|
|
"rate_plan_id": "plan-123",
|
|
"profile_id": "profile-456",
|
|
"cycle_start": "2026-01-01T00:00:00Z",
|
|
"cycle_end": "2026-01-31T23:59:59Z",
|
|
"data_used": int64(1024),
|
|
"voice_used": int64(300),
|
|
"sms_used": int64(50),
|
|
}
|
|
|
|
reqBody, _ := json.Marshal(usageReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/usage", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusCreated, w.Code)
|
|
|
|
var usage repository.RatePlanUsage
|
|
err := json.Unmarshal(w.Body.Bytes(), &usage)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), "plan-123", usage.RatePlanID)
|
|
assert.Equal(suite.T(), "profile-456", usage.ProfileID)
|
|
assert.Equal(suite.T(), int64(1024), usage.DataUsed)
|
|
|
|
usageID := usage.ID
|
|
|
|
// 2. Get the usage record
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/usage/%s", usageID), nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
var retrievedUsage repository.RatePlanUsage
|
|
err = json.Unmarshal(w.Body.Bytes(), &retrievedUsage)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), usageID, retrievedUsage.ID)
|
|
|
|
// 3. List usage records
|
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/usage", nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
var usageList []repository.RatePlanUsage
|
|
err = json.Unmarshal(w.Body.Bytes(), &usageList)
|
|
require.NoError(suite.T(), err)
|
|
assert.Greater(suite.T(), len(usageList), 0)
|
|
}
|
|
|
|
// TestSubscriptionLifecycle tests subscription management
|
|
func (suite *APIIntegrationTestSuite) TestSubscriptionLifecycle() {
|
|
// 1. Create a subscription
|
|
subReq := map[string]interface{}{
|
|
"profile_id": "profile-789",
|
|
"rate_plan_id": "plan-abc",
|
|
"status": "active",
|
|
"billing_cycle": "monthly",
|
|
"next_billing_date": "2026-02-01T00:00:00Z",
|
|
"auto_renew": true,
|
|
"applied_discounts": []string{"new-user-10"},
|
|
}
|
|
|
|
reqBody, _ := json.Marshal(subReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/subscriptions", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusCreated, w.Code)
|
|
|
|
var subscription repository.RatePlanSubscription
|
|
err := json.Unmarshal(w.Body.Bytes(), &subscription)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), "profile-789", subscription.ProfileID)
|
|
assert.Equal(suite.T(), "plan-abc", subscription.RatePlanID)
|
|
assert.Equal(suite.T(), repository.SubscriptionStatus("active"), subscription.Status)
|
|
|
|
subscriptionID := subscription.ID
|
|
|
|
// 2. Get the subscription
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/subscriptions/%s", subscriptionID), nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
var retrievedSubscription repository.RatePlanSubscription
|
|
err = json.Unmarshal(w.Body.Bytes(), &retrievedSubscription)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), subscriptionID, retrievedSubscription.ID)
|
|
|
|
// 3. Cancel the subscription
|
|
req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/subscriptions/%s/cancel", subscriptionID), nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
// 4. Verify subscription is cancelled
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/subscriptions/%s", subscriptionID), nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
var cancelledSubscription repository.RatePlanSubscription
|
|
err = json.Unmarshal(w.Body.Bytes(), &cancelledSubscription)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), repository.SubscriptionStatus("cancelled"), cancelledSubscription.Status)
|
|
}
|
|
|
|
// TestHealthCheck tests the health endpoint
|
|
func (suite *APIIntegrationTestSuite) TestHealthCheck() {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusOK, w.Code)
|
|
|
|
var health map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &health)
|
|
require.NoError(suite.T(), err)
|
|
assert.Equal(suite.T(), "healthy", health["status"])
|
|
assert.Contains(suite.T(), health, "timestamp")
|
|
}
|
|
|
|
// TestErrorHandling tests API error handling
|
|
func (suite *APIIntegrationTestSuite) TestErrorHandling() {
|
|
// Test getting non-existent usage
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage/non-existent", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusNotFound, w.Code)
|
|
|
|
var errorResponse map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
|
|
require.NoError(suite.T(), err)
|
|
assert.Contains(suite.T(), errorResponse, "error")
|
|
|
|
// Test invalid request body
|
|
reqBody := []byte("{invalid json}")
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/usage", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// TestConcurrentRequests tests concurrent API requests
|
|
func (suite *APIIntegrationTestSuite) TestConcurrentRequests() {
|
|
// Create some test data first
|
|
usage := repository.RatePlanUsage{
|
|
RatePlanID: "test-plan",
|
|
ProfileID: "test-profile",
|
|
CycleStart: time.Now(),
|
|
CycleEnd: time.Now().Add(30 * 24 * time.Hour),
|
|
DataUsed: 100,
|
|
VoiceUsed: 50,
|
|
SMSUsed: 10,
|
|
}
|
|
err := suite.db.Create(&usage).Error
|
|
require.NoError(suite.T(), err)
|
|
|
|
// Test concurrent requests
|
|
const numGoroutines = 10
|
|
const numRequests = 100
|
|
|
|
errChan := make(chan error, numGoroutines)
|
|
|
|
for i := 0; i < numGoroutines; i++ {
|
|
go func() {
|
|
for j := 0; j < numRequests/numGoroutines; j++ {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/usage", nil)
|
|
w := httptest.NewRecorder()
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
errChan <- fmt.Errorf("unexpected status code: %d", w.Code)
|
|
return
|
|
}
|
|
}
|
|
errChan <- nil
|
|
}()
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < numGoroutines; i++ {
|
|
err := <-errChan
|
|
assert.NoError(suite.T(), err)
|
|
}
|
|
}
|
|
|
|
// TestRatePlanUsageValidation tests input validation
|
|
func (suite *APIIntegrationTestSuite) TestRatePlanUsageValidation() {
|
|
// Test missing required fields
|
|
invalidReq := map[string]interface{}{
|
|
"profile_id": "profile-123",
|
|
// Missing rate_plan_id
|
|
}
|
|
|
|
reqBody, _ := json.Marshal(invalidReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/usage", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusBadRequest, w.Code)
|
|
|
|
var errorResponse map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
|
|
require.NoError(suite.T(), err)
|
|
assert.Contains(suite.T(), errorResponse, "error")
|
|
|
|
// Test invalid data types
|
|
invalidReq2 := map[string]interface{}{
|
|
"rate_plan_id": "plan-123",
|
|
"profile_id": "profile-123",
|
|
"data_used": "not-a-number", // Should be int64
|
|
}
|
|
|
|
reqBody, _ = json.Marshal(invalidReq2)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/usage", bytes.NewBuffer(reqBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
|
|
suite.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(suite.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// Handler implementations
|
|
func (suite *APIIntegrationTestSuite) createUsage(c *gin.Context) {
|
|
var usage repository.RatePlanUsage
|
|
if err := c.ShouldBindJSON(&usage); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Generate ID if not provided
|
|
if usage.ID == "" {
|
|
usage.ID = fmt.Sprintf("usage-%d", time.Now().UnixNano())
|
|
}
|
|
|
|
// Set timestamps
|
|
now := time.Now()
|
|
usage.LastUpdated = now
|
|
|
|
if err := suite.db.Create(&usage).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, usage)
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) getUsage(c *gin.Context) {
|
|
var usage repository.RatePlanUsage
|
|
if err := suite.db.First(&usage, "id = ?", c.Param("id")).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "usage record not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, usage)
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) listUsage(c *gin.Context) {
|
|
var usage []repository.RatePlanUsage
|
|
if err := suite.db.Find(&usage).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, usage)
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) createSubscription(c *gin.Context) {
|
|
var subscription repository.RatePlanSubscription
|
|
if err := c.ShouldBindJSON(&subscription); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Generate ID if not provided
|
|
if subscription.ID == "" {
|
|
subscription.ID = fmt.Sprintf("sub-%d", time.Now().UnixNano())
|
|
}
|
|
|
|
// Set timestamps
|
|
now := time.Now()
|
|
subscription.StartedAt = now
|
|
subscription.CreatedAt = now
|
|
subscription.UpdatedAt = now
|
|
subscription.CurrentCycle = now
|
|
|
|
if err := suite.db.Create(&subscription).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, subscription)
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) getSubscription(c *gin.Context) {
|
|
var subscription repository.RatePlanSubscription
|
|
if err := suite.db.First(&subscription, "id = ?", c.Param("id")).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "subscription not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subscription)
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) cancelSubscription(c *gin.Context) {
|
|
if err := suite.db.Model(&repository.RatePlanSubscription{}).Where("id = ?", c.Param("id")).Updates(map[string]interface{}{
|
|
"status": "cancelled",
|
|
"ended_at": time.Now(),
|
|
"updated_at": time.Now(),
|
|
}).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "cancelled"})
|
|
}
|
|
|
|
func (suite *APIIntegrationTestSuite) healthCheck(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"status": "healthy",
|
|
"timestamp": time.Now(),
|
|
"version": "1.0.0",
|
|
})
|
|
}
|
|
|
|
// TearDownSuite cleans up the test suite
|
|
func (suite *APIIntegrationTestSuite) TearDownSuite() {
|
|
sqlDB, err := suite.db.DB()
|
|
if err == nil {
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
// TestAPIIntegrationSuite runs the integration test suite
|
|
func TestAPIIntegrationSuite(t *testing.T) {
|
|
suite.Run(t, new(APIIntegrationTestSuite))
|
|
}
|