Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ docker.start:
docker.stop:
docker compose --profile all down

docker.e2e.start:
@echo "Starting Redis and cae-resp-proxy for E2E tests..."
docker compose --profile e2e up -d --quiet-pull
@echo "Waiting for services to be ready..."
@sleep 3
@echo "Services ready!"

docker.e2e.stop:
@echo "Stopping E2E services..."
docker compose --profile e2e down

test:
$(MAKE) docker.start
@if [ -z "$(REDIS_VERSION)" ]; then \
Expand Down Expand Up @@ -66,7 +77,31 @@ bench:
export REDIS_VERSION=$(REDIS_VERSION) && \
go test ./... -test.run=NONE -test.bench=. -test.benchmem -skip Example

.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt
test.e2e:
@echo "Running E2E tests with auto-start proxy..."
$(MAKE) docker.e2e.start
@echo "Running tests..."
@E2E_SCENARIO_TESTS=true go test -v ./maintnotifications/e2e/ -timeout 30m || ($(MAKE) docker.e2e.stop && exit 1)
$(MAKE) docker.e2e.stop
@echo "E2E tests completed!"

test.e2e.docker:
@echo "Running Docker-compatible E2E tests..."
$(MAKE) docker.e2e.start
@echo "Running unified injector tests..."
@E2E_SCENARIO_TESTS=true go test -v -run "TestUnifiedInjector|TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/ -timeout 10m || ($(MAKE) docker.e2e.stop && exit 1)
$(MAKE) docker.e2e.stop
@echo "Docker E2E tests completed!"

test.e2e.logic:
@echo "Running E2E logic tests (no proxy required)..."
@E2E_SCENARIO_TESTS=true \
REDIS_ENDPOINTS_CONFIG_PATH=/tmp/test_endpoints_verify.json \
FAULT_INJECTION_API_URL=http://localhost:8080 \
go test -v -run "TestCreateTestFaultInjectorLogic|TestFaultInjectorClientCreation" ./maintnotifications/e2e/
@echo "Logic tests completed!"

.PHONY: all test test.ci test.ci.skip-vectorsets bench fmt test.e2e test.e2e.logic docker.e2e.start docker.e2e.stop

build:
export RE_CLUSTER=$(RE_CLUSTER) && \
Expand Down
40 changes: 40 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ services:
- sentinel
- all-stack
- all
- e2e

osscluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
Expand All @@ -39,6 +40,45 @@ services:
- all-stack
- all

cae-resp-proxy:
image: redislabs/client-resp-proxy:latest
container_name: cae-resp-proxy
environment:
- TARGET_HOST=redis
- TARGET_PORT=6379
- LISTEN_PORT=7000,7001,7002 # Multiple proxy nodes for cluster simulation
- LISTEN_HOST=0.0.0.0
- API_PORT=3000
- DEFAULT_INTERCEPTORS=cluster,hitless
ports:
- "7000:7000" # Proxy node 1 (host:container)
- "7001:7001" # Proxy node 2 (host:container)
- "7002:7002" # Proxy node 3 (host:container)
- "8100:3000" # HTTP API port (host:container)
depends_on:
- redis
profiles:
- e2e
- all

proxy-fault-injector:
image: golang:1.23-alpine
container_name: proxy-fault-injector
working_dir: /app
environment:
- CGO_ENABLED=0
command: >
sh -c "go run ./maintnotifications/e2e/cmd/proxy-fi-server/main.go --listen 0.0.0.0:5000 --proxy-api-url http://cae-resp-proxy:3000"
ports:
- "5000:5000" # Fault injector API port
depends_on:
- cae-resp-proxy
volumes:
- ".:/app"
profiles:
- e2e
- all

sentinel-cluster:
image: ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.4.0}
platform: linux/amd64
Expand Down
33 changes: 33 additions & 0 deletions maintnotifications/e2e/cmd/proxy-fi-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /build

# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the proxy-fi-server binary
RUN cd maintnotifications/e2e/cmd/proxy-fi-server && \
CGO_ENABLED=0 GOOS=linux go build -o /proxy-fi-server .

# Runtime stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /app

# Copy the binary from builder
COPY --from=builder /proxy-fi-server .

# Expose the fault injector API port
EXPOSE 5000

# Run the server
ENTRYPOINT ["/app/proxy-fi-server"]
CMD ["--listen", "0.0.0.0:5000", "--proxy-api-port", "3000"]

43 changes: 43 additions & 0 deletions maintnotifications/e2e/cmd/proxy-fi-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"flag"
"fmt"
"os"
"os/signal"
"syscall"

e2e "github.com/redis/go-redis/v9/maintnotifications/e2e"
)

func main() {
listenAddr := flag.String("listen", "0.0.0.0:5000", "Address to listen on for fault injector API")
proxyAPIURL := flag.String("proxy-api-url", "http://localhost:8100", "URL of the cae-resp-proxy API")
flag.Parse()

fmt.Printf("Starting Proxy Fault Injector Server...\n")
fmt.Printf(" Listen address: %s\n", *listenAddr)
fmt.Printf(" Proxy API URL: %s\n", *proxyAPIURL)

server := e2e.NewProxyFaultInjectorServerWithURL(*listenAddr, *proxyAPIURL)
if err := server.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start server: %v\n", err)
os.Exit(1)
}

fmt.Printf("Proxy Fault Injector Server started successfully\n")
fmt.Printf("Fault Injector API available at http://%s\n", *listenAddr)

// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan

fmt.Println("\nShutting down...")
if err := server.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "Error during shutdown: %v\n", err)
os.Exit(1)
}
fmt.Println("Server stopped")
}

182 changes: 182 additions & 0 deletions maintnotifications/e2e/config_autostart_logic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package e2e

import (
"os"
"testing"
)

// TestCreateTestFaultInjectorLogic_WithoutEnv verifies the auto-start logic
// when REDIS_ENDPOINTS_CONFIG_PATH is not set
func TestCreateTestFaultInjectorLogic_WithoutEnv(t *testing.T) {
// Save original environment
origConfigPath := os.Getenv("REDIS_ENDPOINTS_CONFIG_PATH")
origFIURL := os.Getenv("FAULT_INJECTION_API_URL")

// Clear environment to simulate no setup
os.Unsetenv("REDIS_ENDPOINTS_CONFIG_PATH")
os.Unsetenv("FAULT_INJECTION_API_URL")

// Restore environment after test
defer func() {
if origConfigPath != "" {
os.Setenv("REDIS_ENDPOINTS_CONFIG_PATH", origConfigPath)
}
if origFIURL != "" {
os.Setenv("FAULT_INJECTION_API_URL", origFIURL)
}
}()

// Test GetEnvConfig - should fail when REDIS_ENDPOINTS_CONFIG_PATH is not set
envConfig, err := GetEnvConfig()
if err == nil {
t.Fatal("Expected GetEnvConfig() to fail when REDIS_ENDPOINTS_CONFIG_PATH is not set")
}
if envConfig != nil {
t.Fatal("Expected envConfig to be nil when GetEnvConfig() fails")
}

t.Log("✅ GetEnvConfig() correctly fails when REDIS_ENDPOINTS_CONFIG_PATH is not set")
t.Log("✅ This means CreateTestFaultInjectorWithCleanup() will auto-start the proxy")
}

// TestCreateTestFaultInjectorLogic_WithEnv verifies the logic
// when REDIS_ENDPOINTS_CONFIG_PATH is set
func TestCreateTestFaultInjectorLogic_WithEnv(t *testing.T) {
// Create a temporary config file
tmpFile := "/tmp/test_endpoints.json"
content := `{
"standalone": {
"endpoints": ["redis://localhost:6379"]
}
}`

if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpFile)

// Save original environment
origConfigPath := os.Getenv("REDIS_ENDPOINTS_CONFIG_PATH")
origFIURL := os.Getenv("FAULT_INJECTION_API_URL")

// Set environment
os.Setenv("REDIS_ENDPOINTS_CONFIG_PATH", tmpFile)
os.Setenv("FAULT_INJECTION_API_URL", "http://test-fi:9999")

// Restore environment after test
defer func() {
if origConfigPath != "" {
os.Setenv("REDIS_ENDPOINTS_CONFIG_PATH", origConfigPath)
} else {
os.Unsetenv("REDIS_ENDPOINTS_CONFIG_PATH")
}
if origFIURL != "" {
os.Setenv("FAULT_INJECTION_API_URL", origFIURL)
} else {
os.Unsetenv("FAULT_INJECTION_API_URL")
}
}()

// Test GetEnvConfig - should succeed when REDIS_ENDPOINTS_CONFIG_PATH is set
envConfig, err := GetEnvConfig()
if err != nil {
t.Fatalf("Expected GetEnvConfig() to succeed when REDIS_ENDPOINTS_CONFIG_PATH is set, got error: %v", err)
}
if envConfig == nil {
t.Fatal("Expected envConfig to be non-nil when GetEnvConfig() succeeds")
}

// Verify the fault injector URL is correct
if envConfig.FaultInjectorURL != "http://test-fi:9999" {
t.Errorf("Expected FaultInjectorURL to be 'http://test-fi:9999', got '%s'", envConfig.FaultInjectorURL)
}

t.Log("✅ GetEnvConfig() correctly succeeds when REDIS_ENDPOINTS_CONFIG_PATH is set")
t.Log("✅ This means CreateTestFaultInjectorWithCleanup() will use the real fault injector")
t.Logf("✅ Fault injector URL: %s", envConfig.FaultInjectorURL)
}

// TestCreateTestFaultInjectorLogic_DefaultFIURL verifies the default fault injector URL
func TestCreateTestFaultInjectorLogic_DefaultFIURL(t *testing.T) {
// Create a temporary config file
tmpFile := "/tmp/test_endpoints2.json"
content := `{
"standalone": {
"endpoints": ["redis://localhost:6379"]
}
}`

if err := os.WriteFile(tmpFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tmpFile)

// Save original environment
origConfigPath := os.Getenv("REDIS_ENDPOINTS_CONFIG_PATH")
origFIURL := os.Getenv("FAULT_INJECTION_API_URL")

// Set only config path, not fault injector URL
os.Setenv("REDIS_ENDPOINTS_CONFIG_PATH", tmpFile)
os.Unsetenv("FAULT_INJECTION_API_URL")

// Restore environment after test
defer func() {
if origConfigPath != "" {
os.Setenv("REDIS_ENDPOINTS_CONFIG_PATH", origConfigPath)
} else {
os.Unsetenv("REDIS_ENDPOINTS_CONFIG_PATH")
}
if origFIURL != "" {
os.Setenv("FAULT_INJECTION_API_URL", origFIURL)
}
}()

// Test GetEnvConfig - should succeed and use default FI URL
envConfig, err := GetEnvConfig()
if err != nil {
t.Fatalf("Expected GetEnvConfig() to succeed, got error: %v", err)
}

// Verify the default fault injector URL
if envConfig.FaultInjectorURL != "http://localhost:8080" {
t.Errorf("Expected default FaultInjectorURL to be 'http://localhost:8080', got '%s'", envConfig.FaultInjectorURL)
}

t.Log("✅ GetEnvConfig() uses default fault injector URL when FAULT_INJECTION_API_URL is not set")
t.Logf("✅ Default fault injector URL: %s", envConfig.FaultInjectorURL)
}

// TestFaultInjectorClientCreation verifies that FaultInjectorClient can be created
func TestFaultInjectorClientCreation(t *testing.T) {
// Test creating client with different URLs
testCases := []struct {
name string
url string
}{
{"localhost", "http://localhost:5000"},
{"with port", "http://test:9999"},
{"with trailing slash", "http://test:9999/"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewFaultInjectorClient(tc.url)
if client == nil {
t.Fatal("Expected non-nil client")
}

// Verify the base URL (should have trailing slash removed)
expectedURL := tc.url
if expectedURL[len(expectedURL)-1] == '/' {
expectedURL = expectedURL[:len(expectedURL)-1]
}

if client.GetBaseURL() != expectedURL {
t.Errorf("Expected base URL '%s', got '%s'", expectedURL, client.GetBaseURL())
}

t.Logf("✅ Client created successfully with URL: %s", client.GetBaseURL())
})
}
}

Loading
Loading