From f8d218448d660efc4b6ad55bff85f82c82f75a0d Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Mon, 20 Apr 2026 20:08:23 +0300 Subject: [PATCH] refactor: Replace interface{} with any and add comprehensive plugin service tests with benchmarks - Replace interface{} with any in sendDeactivationRequest method signature in es2_helpers.go - Add plugin_service_test.go with setupPluginTest helper using in-memory SQLite and gorm.AutoMigrate - Add TestPluginService_List with test cases for all/enabled/disabled/by-type filtering and no matches - Add TestPluginService_Install with tests for successful install, duplicate prevention, and missing fields --- .../internal/services/es2_helpers.go | 2 +- .../internal/services/plugin_service_test.go | 256 ++++++++++++++++++ apps/carrier-connector/go.mod | 7 +- apps/carrier-connector/go.sum | 13 +- 4 files changed, 265 insertions(+), 13 deletions(-) create mode 100644 apps/api-server/internal/services/plugin_service_test.go diff --git a/apps/api-server/internal/services/es2_helpers.go b/apps/api-server/internal/services/es2_helpers.go index 6b4ee7e..d03703e 100644 --- a/apps/api-server/internal/services/es2_helpers.go +++ b/apps/api-server/internal/services/es2_helpers.go @@ -39,7 +39,7 @@ func (e *ES2Service) sendActivationRequest(ctx context.Context, req ActivationRe return nil } -func (e *ES2Service) sendDeactivationRequest(ctx context.Context, req interface{}) error { +func (e *ES2Service) sendDeactivationRequest(ctx context.Context, req any) error { jsonData, err := json.Marshal(req) if err != nil { return fmt.Errorf("failed to marshal deactivation request: %w", err) diff --git a/apps/api-server/internal/services/plugin_service_test.go b/apps/api-server/internal/services/plugin_service_test.go new file mode 100644 index 0000000..2e0d6b5 --- /dev/null +++ b/apps/api-server/internal/services/plugin_service_test.go @@ -0,0 +1,256 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/database" + "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func setupPluginTest(t *testing.T) (*database.Database, *PluginService) { + // Use in-memory SQLite for testing + gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + require.NoError(t, err) + + // Migrate plugin table + err = gormDB.AutoMigrate(&models.Plugin{}) + require.NoError(t, err) + + // Create database wrapper + db := &database.Database{DB: gormDB} + + // Create service + service := NewPluginService(db) + + return db, service +} + +func TestPluginService_List(t *testing.T) { + db, service := setupPluginTest(t) + + // Create test plugins + plugins := []models.Plugin{ + { + Name: "test-plugin-1", + Version: "1.0.0", + Description: "Test plugin 1", + Author: "Test Author", + Type: "monitoring", + Enabled: true, + Status: "installed", + }, + { + Name: "test-plugin-2", + Version: "2.0.0", + Description: "Test plugin 2", + Author: "Test Author", + Type: "security", + Enabled: false, + Status: "installed", + }, + } + + for _, plugin := range plugins { + err := db.DB.Create(&plugin).Error + require.NoError(t, err) + } + + t.Run("List all plugins", func(t *testing.T) { + result, err := service.List(context.Background(), PluginFilter{}) + require.NoError(t, err) + assert.Len(t, result, 2) + }) + + t.Run("List enabled plugins only", func(t *testing.T) { + enabled := true + result, err := service.List(context.Background(), PluginFilter{ + Enabled: &enabled, + }) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.True(t, result[0].Enabled) + }) + + t.Run("List disabled plugins only", func(t *testing.T) { + enabled := false + result, err := service.List(context.Background(), PluginFilter{ + Enabled: &enabled, + }) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.False(t, result[0].Enabled) + }) + + t.Run("List plugins by type", func(t *testing.T) { + result, err := service.List(context.Background(), PluginFilter{ + Type: "monitoring", + }) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "monitoring", result[0].Type) + }) + + t.Run("List plugins with no matches", func(t *testing.T) { + result, err := service.List(context.Background(), PluginFilter{ + Type: "nonexistent", + }) + require.NoError(t, err) + assert.Len(t, result, 0) + }) +} + +func TestPluginService_Install(t *testing.T) { + _, service := setupPluginTest(t) + + t.Run("Install new plugin successfully", func(t *testing.T) { + configJSON, _ := json.Marshal(map[string]interface{}{"key": "value"}) + input := InstallPluginInput{ + Name: "new-plugin", + Version: "1.0.0", + Description: "A new plugin", + Author: "Test Author", + Type: "monitoring", + Config: string(configJSON), + } + + result, err := service.Install(context.Background(), input) + require.NoError(t, err) + + assert.Equal(t, input.Name, result.Name) + assert.Equal(t, input.Version, result.Version) + assert.Equal(t, input.Description, result.Description) + assert.Equal(t, input.Author, result.Author) + assert.Equal(t, input.Type, result.Type) + assert.True(t, result.Enabled) + assert.Equal(t, "installed", result.Status) + assert.NotZero(t, result.ID) + assert.NotZero(t, result.InstalledAt) + }) + + t.Run("Install duplicate plugin should fail", func(t *testing.T) { + input := InstallPluginInput{ + Name: "duplicate-plugin", + Version: "1.0.0", + Description: "Duplicate plugin", + Author: "Test Author", + Type: "monitoring", + } + + // First installation should succeed + _, err := service.Install(context.Background(), input) + require.NoError(t, err) + + // Second installation should fail + _, err = service.Install(context.Background(), input) + assert.Error(t, err) + assert.Contains(t, err.Error(), "UNIQUE constraint failed") + }) + + t.Run("Install plugin with missing required fields", func(t *testing.T) { + input := InstallPluginInput{ + // Missing name + Version: "1.0.0", + } + + _, err := service.Install(context.Background(), input) + // The service might not validate this, so just check it doesn't panic + if err != nil { + assert.Error(t, err) + } + }) +} + +func TestPluginService_Get(t *testing.T) { + db, service := setupPluginTest(t) + + // Create a plugin + plugin := models.Plugin{ + Name: "test-plugin", + Version: "1.0.0", + Description: "Test plugin", + Author: "Test Author", + Type: "monitoring", + Enabled: true, + Status: "installed", + } + err := db.DB.Create(&plugin).Error + require.NoError(t, err) + + t.Run("Get existing plugin", func(t *testing.T) { + result, err := service.Get(context.Background(), plugin.ID) + require.NoError(t, err) + assert.Equal(t, plugin.Name, result.Name) + assert.Equal(t, plugin.Version, result.Version) + assert.Equal(t, plugin.Description, result.Description) + }) + + t.Run("Get nonexistent plugin", func(t *testing.T) { + _, err := service.Get(context.Background(), 999) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestPluginService_Uninstall(t *testing.T) { + db, service := setupPluginTest(t) + + // Create a plugin + plugin := models.Plugin{ + Name: "test-plugin", + Version: "1.0.0", + Description: "Test plugin", + Enabled: true, + Status: "installed", + } + err := db.DB.Create(&plugin).Error + require.NoError(t, err) + + t.Run("Uninstall existing plugin", func(t *testing.T) { + err := service.Uninstall(context.Background(), plugin.ID) + require.NoError(t, err) + + // Verify plugin is deleted + var deletedPlugin models.Plugin + err = db.DB.First(&deletedPlugin, plugin.ID).Error + assert.Error(t, err) // Should not find the plugin + assert.Contains(t, err.Error(), "record not found") + }) + + t.Run("Uninstall nonexistent plugin", func(t *testing.T) { + err := service.Uninstall(context.Background(), 999) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +// Benchmark tests +func BenchmarkPluginService_List(b *testing.B) { + db, service := setupPluginTest(&testing.T{}) + + // Create test plugins + for i := 0; i < 100; i++ { + plugin := models.Plugin{ + Name: fmt.Sprintf("plugin-%d", i), + Version: "1.0.0", + Description: "Test plugin", + Enabled: i%2 == 0, + Status: "installed", + } + db.DB.Create(&plugin) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := service.List(context.Background(), PluginFilter{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/apps/carrier-connector/go.mod b/apps/carrier-connector/go.mod index 755ed2a..800dbdc 100644 --- a/apps/carrier-connector/go.mod +++ b/apps/carrier-connector/go.mod @@ -10,7 +10,8 @@ require ( require ( github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.41.0 // indirect + github.com/mattn/go-isatty v0.0.21 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/time v0.15.0 // indirect ) diff --git a/apps/carrier-connector/go.sum b/apps/carrier-connector/go.sum index f19a7bc..1f9aadc 100644 --- a/apps/carrier-connector/go.sum +++ b/apps/carrier-connector/go.sum @@ -4,14 +4,9 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI= github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=