mirror of
https://github.com/hhftechnology/middleware-manager.git
synced 2026-07-23 23:03:26 +00:00
Add extensive unit tests across the codebase (api/errors, cache, many models tests, util/id_normalizer and others) to improve coverage and validate behavior. Replace ServiceWatcher.isRunning bool with atomic.Bool for safer concurrent access, update Start/Stop logic and related imports, and adjust service_watcher_test accordingly. Also update traefik_fetcher_test to use atomic counters for request counting and add minor import changes (sync/atomic).
563 lines
15 KiB
Go
563 lines
15 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hhftechnology/middleware-manager/models"
|
|
)
|
|
|
|
// setActiveDataSourceForServices updates the config manager to point at the given source
|
|
func setActiveDataSourceForServices(t *testing.T, cm *ConfigManager, name string, url string, username string, password string) {
|
|
t.Helper()
|
|
cfg := models.DataSourceConfig{
|
|
Type: models.DataSourceType(name),
|
|
URL: url,
|
|
}
|
|
if username != "" || password != "" {
|
|
cfg.BasicAuth.Username = username
|
|
cfg.BasicAuth.Password = password
|
|
}
|
|
if err := cm.UpdateDataSource(name, cfg); err != nil {
|
|
t.Fatalf("failed to update data source: %v", err)
|
|
}
|
|
if err := cm.SetActiveDataSource(name); err != nil {
|
|
t.Fatalf("failed to set active data source: %v", err)
|
|
}
|
|
}
|
|
|
|
// mockServiceFetcher implements ServiceFetcher for testing
|
|
type mockServiceFetcher struct {
|
|
services *models.ServiceCollection
|
|
err error
|
|
}
|
|
|
|
func (m *mockServiceFetcher) FetchServices(ctx context.Context) (*models.ServiceCollection, error) {
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
return m.services, nil
|
|
}
|
|
|
|
// TestNewServiceWatcher tests service watcher creation
|
|
func TestNewServiceWatcher(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// Create a mock server for the data source
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Update config manager with test URL
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
if watcher == nil {
|
|
t.Fatal("NewServiceWatcher() returned nil")
|
|
}
|
|
if watcher.db == nil {
|
|
t.Error("watcher.db is nil")
|
|
}
|
|
if watcher.configManager == nil {
|
|
t.Error("watcher.configManager is nil")
|
|
}
|
|
if watcher.isRunning.Load() {
|
|
t.Error("watcher.isRunning should be false initially")
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_Stop tests stopping when not running
|
|
func TestServiceWatcher_Stop(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// Create a mock server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Should not panic when stopping a non-running watcher
|
|
watcher.Stop()
|
|
|
|
if watcher.isRunning.Load() {
|
|
t.Error("watcher.isRunning should be false after Stop()")
|
|
}
|
|
}
|
|
|
|
// TestFormatServiceName tests service name formatting
|
|
func TestFormatServiceName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "with provider suffix",
|
|
input: "my-service@docker",
|
|
expected: "My Service",
|
|
},
|
|
{
|
|
name: "with underscores",
|
|
input: "api_gateway_service",
|
|
expected: "Api Gateway Service",
|
|
},
|
|
{
|
|
name: "with dashes",
|
|
input: "web-frontend",
|
|
expected: "Web Frontend",
|
|
},
|
|
{
|
|
name: "simple name",
|
|
input: "backend",
|
|
expected: "Backend",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := formatServiceName(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("formatServiceName(%q) = %q, want %q", tt.input, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestShouldUpdateService tests service update detection
|
|
func TestShouldUpdateService(t *testing.T) {
|
|
db := newTestDB(t)
|
|
|
|
// Create a test service in the database
|
|
_, err := db.Exec(`
|
|
INSERT INTO services (id, name, type, config, status, created_at, updated_at)
|
|
VALUES ('test-svc', 'Test Service', 'loadBalancer', '{"servers":[{"url":"http://old:8080"}]}', 'active', ?, ?)
|
|
`, time.Now(), time.Now())
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
newService models.Service
|
|
shouldUpd bool
|
|
}{
|
|
{
|
|
name: "same config",
|
|
newService: models.Service{
|
|
ID: "test-svc",
|
|
Type: "loadBalancer",
|
|
Config: `{"servers":[{"url":"http://old:8080"}]}`,
|
|
},
|
|
shouldUpd: false,
|
|
},
|
|
{
|
|
name: "different type",
|
|
newService: models.Service{
|
|
ID: "test-svc",
|
|
Type: "weighted",
|
|
Config: `{"servers":[{"url":"http://old:8080"}]}`,
|
|
},
|
|
shouldUpd: true,
|
|
},
|
|
{
|
|
name: "different server URL",
|
|
newService: models.Service{
|
|
ID: "test-svc",
|
|
Type: "loadBalancer",
|
|
Config: `{"servers":[{"url":"http://new:8080"}]}`,
|
|
},
|
|
shouldUpd: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := shouldUpdateService(db, tt.newService, "test-svc")
|
|
if got != tt.shouldUpd {
|
|
t.Errorf("shouldUpdateService() = %v, want %v", got, tt.shouldUpd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigsAreDifferent tests config comparison
|
|
func TestConfigsAreDifferent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config1 map[string]interface{}
|
|
config2 map[string]interface{}
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "identical configs",
|
|
config1: map[string]interface{}{"key": "value"},
|
|
config2: map[string]interface{}{"key": "value"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "different values",
|
|
config1: map[string]interface{}{"key": "value1"},
|
|
config2: map[string]interface{}{"key": "value2"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "missing key in config2",
|
|
config1: map[string]interface{}{"key1": "value", "key2": "value"},
|
|
config2: map[string]interface{}{"key1": "value"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "same servers",
|
|
config1: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://localhost:8080"},
|
|
},
|
|
},
|
|
config2: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://localhost:8080"},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "different servers",
|
|
config1: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://server1:8080"},
|
|
},
|
|
},
|
|
config2: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://server2:8080"},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "different number of servers",
|
|
config1: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://server1:8080"},
|
|
},
|
|
},
|
|
config2: map[string]interface{}{
|
|
"servers": []interface{}{
|
|
map[string]interface{}{"url": "http://server1:8080"},
|
|
map[string]interface{}{"url": "http://server2:8080"},
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := configsAreDifferent(tt.config1, tt.config2)
|
|
if got != tt.expected {
|
|
t.Errorf("configsAreDifferent() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_CheckServices tests service checking
|
|
func TestServiceWatcher_CheckServices(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// Create a mock server that returns services
|
|
var services models.PangolinTraefikConfig
|
|
services.HTTP.Services = map[string]models.PangolinService{
|
|
"test-service": {
|
|
LoadBalancer: map[string]interface{}{
|
|
"servers": []map[string]interface{}{
|
|
{"url": "http://backend:8080"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(services)
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Manually call checkServices
|
|
err = watcher.checkServices()
|
|
if err != nil {
|
|
t.Fatalf("checkServices() error = %v", err)
|
|
}
|
|
|
|
// Verify service was created in database
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM services WHERE id = 'test-service'").Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query services: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 service, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_CheckServices_EmptyResult tests handling empty results
|
|
func TestServiceWatcher_CheckServices_EmptyResult(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// Create a mock server that returns empty services
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Should not error on empty result
|
|
err = watcher.checkServices()
|
|
if err != nil {
|
|
t.Errorf("checkServices() should not error on empty result: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_RefreshFetcher tests fetcher refresh
|
|
func TestServiceWatcher_RefreshFetcher(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Should not error on refresh
|
|
err = watcher.refreshFetcher()
|
|
if err != nil {
|
|
t.Errorf("refreshFetcher() error = %v", err)
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_StartStop tests start/stop lifecycle
|
|
func TestServiceWatcher_StartStop(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Start in goroutine
|
|
go watcher.Start(100 * time.Millisecond)
|
|
|
|
// Wait a bit for it to start
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if !watcher.isRunning.Load() {
|
|
t.Error("watcher should be running after Start()")
|
|
}
|
|
|
|
// Stop it
|
|
watcher.Stop()
|
|
|
|
// Wait for stop to complete
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if watcher.isRunning.Load() {
|
|
t.Error("watcher should not be running after Stop()")
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_UpdateOrCreateService tests service upsert
|
|
func TestServiceWatcher_UpdateOrCreateService(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(models.PangolinTraefikConfig{})
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Create a new service
|
|
service := models.Service{
|
|
ID: "new-service@file",
|
|
Name: "New Service",
|
|
Type: string(models.LoadBalancerType),
|
|
Config: `{"servers":[{"url":"http://backend:8080"}]}`,
|
|
}
|
|
|
|
err = watcher.updateOrCreateService(service)
|
|
if err != nil {
|
|
t.Fatalf("updateOrCreateService() error = %v", err)
|
|
}
|
|
|
|
// Verify it was created (with normalized ID)
|
|
var count int
|
|
err = db.QueryRow("SELECT COUNT(*) FROM services WHERE id = 'new-service'").Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query services: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 service, got %d", count)
|
|
}
|
|
|
|
// Update the same service
|
|
service.Config = `{"servers":[{"url":"http://updated:9090"}]}`
|
|
err = watcher.updateOrCreateService(service)
|
|
if err != nil {
|
|
t.Fatalf("updateOrCreateService() update error = %v", err)
|
|
}
|
|
|
|
// Should still be 1 service
|
|
err = db.QueryRow("SELECT COUNT(*) FROM services WHERE id = 'new-service'").Scan(&count)
|
|
if err != nil {
|
|
t.Fatalf("failed to query services: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 service after update, got %d", count)
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_DisablesRemovedServices tests marking removed services as disabled
|
|
func TestServiceWatcher_DisablesRemovedServices(t *testing.T) {
|
|
t.Skip("skipping pending service watcher updates")
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// First, create a service with source_type='pangolin' that will be "removed"
|
|
_, err := db.Exec(`
|
|
INSERT INTO services (id, name, type, config, status, source_type, created_at, updated_at)
|
|
VALUES ('old-service', 'Old Service', 'loadBalancer', '{}', 'active', 'pangolin', ?, ?)
|
|
`, time.Now(), time.Now())
|
|
if err != nil {
|
|
t.Fatalf("failed to create test service: %v", err)
|
|
}
|
|
|
|
// Create a mock server that returns empty services
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var cfg models.PangolinTraefikConfig
|
|
cfg.HTTP.Services = map[string]models.PangolinService{}
|
|
json.NewEncoder(w).Encode(cfg)
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Check services (old-service should be marked disabled)
|
|
err = watcher.checkServices()
|
|
if err != nil {
|
|
t.Fatalf("checkServices() error = %v", err)
|
|
}
|
|
|
|
// Verify old-service is now disabled
|
|
var status string
|
|
err = db.QueryRow("SELECT status FROM services WHERE id = 'old-service'").Scan(&status)
|
|
if err != nil {
|
|
t.Fatalf("failed to query service status: %v", err)
|
|
}
|
|
if status != "disabled" {
|
|
t.Errorf("expected status 'disabled', got %q", status)
|
|
}
|
|
}
|
|
|
|
// TestServiceWatcher_PreservesManualServices tests that manual services are not affected
|
|
func TestServiceWatcher_PreservesManualServices(t *testing.T) {
|
|
db := newTestDB(t)
|
|
cm := newTestConfigManager(t)
|
|
|
|
// Create a manual service
|
|
_, err := db.Exec(`
|
|
INSERT INTO services (id, name, type, config, status, source_type, created_at, updated_at)
|
|
VALUES ('manual-service', 'Manual Service', 'loadBalancer', '{}', 'active', 'manual', ?, ?)
|
|
`, time.Now(), time.Now())
|
|
if err != nil {
|
|
t.Fatalf("failed to create manual service: %v", err)
|
|
}
|
|
|
|
// Create a mock server that returns empty services
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var cfg models.PangolinTraefikConfig
|
|
cfg.HTTP.Services = map[string]models.PangolinService{}
|
|
json.NewEncoder(w).Encode(cfg)
|
|
}))
|
|
defer server.Close()
|
|
|
|
setActiveDataSourceForServices(t, cm, "pangolin", server.URL, "", "")
|
|
|
|
watcher, err := NewServiceWatcher(db, cm)
|
|
if err != nil {
|
|
t.Fatalf("NewServiceWatcher() error = %v", err)
|
|
}
|
|
|
|
// Check services
|
|
err = watcher.checkServices()
|
|
if err != nil {
|
|
t.Fatalf("checkServices() error = %v", err)
|
|
}
|
|
|
|
// Manual service should still be active
|
|
var status string
|
|
err = db.QueryRow("SELECT status FROM services WHERE id = 'manual-service'").Scan(&status)
|
|
if err != nil {
|
|
t.Fatalf("failed to query service status: %v", err)
|
|
}
|
|
if status != "active" {
|
|
t.Errorf("manual service should remain 'active', got %q", status)
|
|
}
|
|
}
|