428 lines
10 KiB
Go
428 lines
10 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.dcentral.systems/toolz/goplt/internal/client"
|
|
"git.dcentral.systems/toolz/goplt/pkg/config"
|
|
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
|
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestMain sets up the test environment before running tests.
|
|
func TestMain(m *testing.M) {
|
|
// Set Gin to test mode once for all tests to avoid race conditions
|
|
gin.SetMode(gin.TestMode)
|
|
// Run tests
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestNewGateway(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg config.ConfigProvider
|
|
log logger.Logger
|
|
factory *client.ServiceClientFactory
|
|
reg registry.ServiceRegistry
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "successful creation",
|
|
cfg: &mockConfigProvider{},
|
|
log: &mockLogger{},
|
|
factory: &client.ServiceClientFactory{},
|
|
reg: &mockServiceRegistry{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "nil config",
|
|
cfg: nil,
|
|
log: &mockLogger{},
|
|
factory: &client.ServiceClientFactory{},
|
|
reg: &mockServiceRegistry{},
|
|
wantErr: false, // NewGateway doesn't validate nil config currently
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gateway, err := NewGateway(tt.cfg, tt.log, tt.factory, tt.reg)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
assert.Nil(t, gateway)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, gateway)
|
|
assert.Equal(t, tt.cfg, gateway.config)
|
|
assert.Equal(t, tt.log, gateway.log)
|
|
assert.Equal(t, tt.factory, gateway.clientFactory)
|
|
assert.Equal(t, tt.reg, gateway.registry)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateway_SetupRoutes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
routes []RouteConfig
|
|
requestPath string
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "route configured",
|
|
routes: []RouteConfig{
|
|
{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
|
|
},
|
|
requestPath: "/api/v1/test",
|
|
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
|
},
|
|
{
|
|
name: "no routes configured",
|
|
routes: []RouteConfig{},
|
|
requestPath: "/api/v1/test",
|
|
expectedStatus: http.StatusNotFound, // NoRoute handler
|
|
},
|
|
{
|
|
name: "unmatched route",
|
|
routes: []RouteConfig{
|
|
{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
|
|
},
|
|
requestPath: "/api/v1/other",
|
|
expectedStatus: http.StatusNotFound, // NoRoute handler
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gateway := &Gateway{
|
|
config: &mockConfigProvider{},
|
|
log: &mockLogger{},
|
|
clientFactory: &client.ServiceClientFactory{},
|
|
registry: &mockServiceRegistry{},
|
|
routes: tt.routes,
|
|
}
|
|
|
|
router := gin.New()
|
|
gateway.SetupRoutes(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateway_handleRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
route RouteConfig
|
|
registrySetup func() *mockServiceRegistry
|
|
requestPath string
|
|
expectedStatus int
|
|
}{
|
|
{
|
|
name: "service discovery failure",
|
|
route: RouteConfig{
|
|
Path: "/api/v1/test",
|
|
Service: "test-service",
|
|
},
|
|
registrySetup: func() *mockServiceRegistry {
|
|
return &mockServiceRegistry{
|
|
discoverError: errors.New("service discovery failed"),
|
|
}
|
|
},
|
|
requestPath: "/api/v1/test",
|
|
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
|
},
|
|
{
|
|
name: "no service instances",
|
|
route: RouteConfig{
|
|
Path: "/api/v1/test",
|
|
Service: "test-service",
|
|
},
|
|
registrySetup: func() *mockServiceRegistry {
|
|
return &mockServiceRegistry{
|
|
instances: []*registry.ServiceInstance{},
|
|
}
|
|
},
|
|
requestPath: "/api/v1/test",
|
|
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
|
},
|
|
// Note: Testing invalid target URL and proxy forwarding requires integration tests
|
|
// with real HTTP servers, as httptest.ResponseRecorder doesn't support CloseNotify
|
|
// which is required by the reverse proxy.
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mockReg := tt.registrySetup()
|
|
gateway := &Gateway{
|
|
config: &mockConfigProvider{},
|
|
log: &mockLogger{},
|
|
clientFactory: &client.ServiceClientFactory{},
|
|
registry: mockReg,
|
|
routes: []RouteConfig{tt.route},
|
|
}
|
|
|
|
router := gin.New()
|
|
gateway.SetupRoutes(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, tt.requestPath, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Note: Proxy forwarding tests should be in integration tests with testcontainers.
|
|
// The reverse proxy requires a real HTTP connection which httptest.ResponseRecorder
|
|
// cannot provide due to missing CloseNotify interface.
|
|
|
|
func TestGateway_handleRoute_AllMethods(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
methods := []string{
|
|
http.MethodGet,
|
|
http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodDelete,
|
|
http.MethodPatch,
|
|
http.MethodOptions,
|
|
http.MethodHead,
|
|
}
|
|
|
|
// Use a registry that will fail discovery to avoid proxy forwarding issues
|
|
mockReg := &mockServiceRegistry{
|
|
instances: []*registry.ServiceInstance{}, // Empty to trigger service unavailable
|
|
}
|
|
|
|
gateway := &Gateway{
|
|
config: &mockConfigProvider{},
|
|
log: &mockLogger{},
|
|
clientFactory: &client.ServiceClientFactory{},
|
|
registry: mockReg,
|
|
routes: []RouteConfig{
|
|
{Path: "/api/v1/test/**", Service: "test-service"}, // Use wildcard pattern
|
|
},
|
|
}
|
|
|
|
router := gin.New()
|
|
gateway.SetupRoutes(router)
|
|
|
|
for _, method := range methods {
|
|
t.Run(method, func(t *testing.T) {
|
|
req := httptest.NewRequest(method, "/api/v1/test", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
// All methods should be handled (route should match, even if service unavailable)
|
|
// New implementation returns 500 for unknown services, not 404
|
|
assert.NotEqual(t, http.StatusNotFound, w.Code, "Route should match for %s", method)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadRoutes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg config.ConfigProvider
|
|
}{
|
|
{
|
|
name: "empty config",
|
|
cfg: &mockConfigProvider{},
|
|
},
|
|
{
|
|
name: "nil config",
|
|
cfg: nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
routes := loadRoutes(tt.cfg)
|
|
// Currently loadRoutes returns empty slice
|
|
// This test ensures it doesn't panic
|
|
assert.NotNil(t, routes)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGateway_NoRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
gateway := &Gateway{
|
|
config: &mockConfigProvider{},
|
|
log: &mockLogger{},
|
|
clientFactory: &client.ServiceClientFactory{},
|
|
registry: &mockServiceRegistry{},
|
|
routes: []RouteConfig{},
|
|
}
|
|
|
|
router := gin.New()
|
|
gateway.SetupRoutes(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/unknown/path", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Route not found", response["error"])
|
|
assert.Equal(t, "/unknown/path", response["path"])
|
|
}
|
|
|
|
// mockConfigProvider implements config.ConfigProvider for testing.
|
|
type mockConfigProvider struct {
|
|
values map[string]any
|
|
}
|
|
|
|
func (m *mockConfigProvider) Get(key string) any {
|
|
if m.values == nil {
|
|
return nil
|
|
}
|
|
return m.values[key]
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetString(key string) string {
|
|
if m.values == nil {
|
|
return ""
|
|
}
|
|
if val, ok := m.values[key].(string); ok {
|
|
return val
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetInt(key string) int {
|
|
if m.values == nil {
|
|
return 0
|
|
}
|
|
if val, ok := m.values[key].(int); ok {
|
|
return val
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetBool(key string) bool {
|
|
if m.values == nil {
|
|
return false
|
|
}
|
|
if val, ok := m.values[key].(bool); ok {
|
|
return val
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetDuration(key string) time.Duration {
|
|
return 0
|
|
}
|
|
|
|
func (m *mockConfigProvider) GetStringSlice(key string) []string {
|
|
if m.values == nil {
|
|
return nil
|
|
}
|
|
if val, ok := m.values[key].([]string); ok {
|
|
return val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockConfigProvider) IsSet(key string) bool {
|
|
if m.values == nil {
|
|
return false
|
|
}
|
|
_, ok := m.values[key]
|
|
return ok
|
|
}
|
|
|
|
func (m *mockConfigProvider) Unmarshal(v any) error {
|
|
return nil
|
|
}
|
|
|
|
// mockLogger implements logger.Logger for testing.
|
|
type mockLogger struct {
|
|
infoLogs []string
|
|
errorLogs []string
|
|
warnLogs []string
|
|
}
|
|
|
|
func (m *mockLogger) Debug(msg string, fields ...logger.Field) {}
|
|
func (m *mockLogger) Info(msg string, fields ...logger.Field) {
|
|
m.infoLogs = append(m.infoLogs, msg)
|
|
}
|
|
func (m *mockLogger) Warn(msg string, fields ...logger.Field) {
|
|
m.warnLogs = append(m.warnLogs, msg)
|
|
}
|
|
func (m *mockLogger) Error(msg string, fields ...logger.Field) {
|
|
m.errorLogs = append(m.errorLogs, msg)
|
|
}
|
|
func (m *mockLogger) With(fields ...logger.Field) logger.Logger {
|
|
return m
|
|
}
|
|
func (m *mockLogger) WithContext(ctx context.Context) logger.Logger {
|
|
return m
|
|
}
|
|
|
|
// mockServiceRegistry implements registry.ServiceRegistry for testing.
|
|
type mockServiceRegistry struct {
|
|
instances []*registry.ServiceInstance
|
|
discoverError error
|
|
registerError error
|
|
deregError error
|
|
}
|
|
|
|
func (m *mockServiceRegistry) Register(ctx context.Context, service *registry.ServiceInstance) error {
|
|
return m.registerError
|
|
}
|
|
|
|
func (m *mockServiceRegistry) Deregister(ctx context.Context, serviceID string) error {
|
|
return m.deregError
|
|
}
|
|
|
|
func (m *mockServiceRegistry) Discover(ctx context.Context, serviceName string) ([]*registry.ServiceInstance, error) {
|
|
if m.discoverError != nil {
|
|
return nil, m.discoverError
|
|
}
|
|
return m.instances, nil
|
|
}
|
|
|
|
func (m *mockServiceRegistry) Watch(ctx context.Context, serviceName string) (<-chan []*registry.ServiceInstance, error) {
|
|
ch := make(chan []*registry.ServiceInstance, 1)
|
|
ch <- m.instances
|
|
close(ch)
|
|
return ch, nil
|
|
}
|
|
|
|
func (m *mockServiceRegistry) Health(ctx context.Context, serviceID string) (*registry.HealthStatus, error) {
|
|
return ®istry.HealthStatus{
|
|
ServiceID: serviceID,
|
|
Status: "healthy",
|
|
}, nil
|
|
}
|