# Test Server Implementation Guide

Complete guide to implementing the hybrid in-process test server for BDD testing.

## Architecture Overview

### Hybrid In-Process Testing

```mermaid
graph TD
    A[BDD Tests] -->|HTTP Requests| B[Test Server]
    B -->|Uses Real Code| C[Actual Server Implementation]
    C -->|Same Process| A
```

**Key Benefits:**
- No external process management
- Real server behavior
- Fast execution
- Reliable startup/shutdown

## Implementation

### Server Structure

```go
// pkg/bdd/testserver/server.go
type Server struct {
    httpServer  *http.Server
    port       int
    baseURL    string
}
```

### Server Construction

```go
func NewServer() *Server {
    return &Server{
        port: 9191,  // Fixed port for consistency
    }
}
```

### Server Startup

```go
func (s *Server) Start() error {
    s.baseURL = fmt.Sprintf("http://localhost:%d", s.port)
    
    // Create real server instance
    cfg := createTestConfig(s.port)
    realServer := server.NewServer(cfg, context.Background())
    
    // Configure HTTP server
    s.httpServer = &http.Server{
        Addr:    fmt.Sprintf(":%d", s.port),
        Handler: realServer.Router(),  // Use real router!
    }
    
    // Start server in goroutine
    go func() {
        if err := s.httpServer.ListenAndServe(); err != nil {
            if err != http.ErrServerClosed {
                log.Error().Err(err).Msg("Test server failed")
            }
        }
    }()
    
    // Wait for server to be ready
    return s.waitForServerReady()
}
```

### Readiness Verification

```go
func (s *Server) waitForServerReady() error {
    maxAttempts := 30
    
    for attempt := 0; attempt < maxAttempts; attempt++ {
        resp, err := http.Get(fmt.Sprintf("%s/api/ready", s.baseURL))
        
        if err == nil && resp.StatusCode == http.StatusOK {
            resp.Body.Close()
            return nil
        }
        
        if resp != nil {
            resp.Body.Close()
        }
        
        time.Sleep(100 * time.Millisecond)
    }
    
    return fmt.Errorf("server did not become ready after %d attempts", maxAttempts)
}
```

### Graceful Shutdown

```go
func (s *Server) Stop() error {
    if s.httpServer == nil {
        return nil
    }
    
    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    return s.httpServer.Shutdown(ctx)
}
```

## Configuration

### Test Configuration Factory

```go
func createTestConfig(port int) *config.Config {
    return &config.Config{
        Server: config.ServerConfig{
            Host: "localhost",
            Port: port,
        },
        Shutdown: config.ShutdownConfig{
            Timeout: 5 * time.Second,
        },
        Logging: config.LoggingConfig{
            JSON:   false,
            Level:  "trace",
        },
        Telemetry: config.TelemetryConfig{
            Enabled: false,  // Disable telemetry in tests
        },
    }
}
```

## Client Implementation

### HTTP Client

```go
// pkg/bdd/testserver/client.go
type Client struct {
    server   *Server
    lastResp *http.Response
    lastBody []byte
}

func NewClient(server *Server) *Client {
    return &Client{
        server: server,
    }
}
```

### Request Method

```go
func (c *Client) Request(method, path string, body []byte) error {
    url := c.server.GetBaseURL() + path
    
    req, err := http.NewRequest(method, url, nil)
    if err != nil {
        return fmt.Errorf("failed to create request: %w", err)
    }
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()
    
    c.lastResp = resp
    c.lastBody, err = io.ReadAll(resp.Body)
    
    return err
}
```

### Response Validation

```go
func (c *Client) ExpectResponseBody(expected string) error {
    if c.lastResp == nil {
        return fmt.Errorf("no response received")
    }
    
    actual := string(c.lastBody)
    actual = strings.TrimSuffix(actual, "\n")  // Critical: trim newline!
    
    if actual != expected {
        return fmt.Errorf("expected response body %q, got %q", expected, actual)
    }
    
    return nil
}

func (c *Client) ExpectStatusCode(expected int) error {
    if c.lastResp == nil {
        return fmt.Errorf("no response received")
    }
    
    if c.lastResp.StatusCode != expected {
        return fmt.Errorf("expected status %d, got %d", 
            expected, c.lastResp.StatusCode)
    }
    
    return nil
}
```

## Test Suite Integration

### Shared Server Pattern

```go
// pkg/bdd/suite.go
var sharedServer *testserver.Server

func InitializeTestSuite(ctx *godog.TestSuiteContext) {
    ctx.BeforeSuite(func() {
        sharedServer = testserver.NewServer()
        if err := sharedServer.Start(); err != nil {
            panic(err)
        }
    })
    
    ctx.AfterSuite(func() {
        if sharedServer != nil {
            sharedServer.Stop()
        }
    })
}

func InitializeScenario(ctx *godog.ScenarioContext) {
    client := testserver.NewClient(sharedServer)
    steps.InitializeAllSteps(ctx, client)
}
```

### Dedicated Server Pattern (for shutdown tests)

```go
func InitializeShutdownTestSuite(ctx *godog.TestSuiteContext) {
    // No shared server for shutdown tests
}

func InitializeShutdownScenario(ctx *godog.ScenarioContext) {
    server := testserver.NewServer()
    client := testserver.NewClient(server)
    
    ctx.BeforeScenario(func(*godog.Scenario) {
        if err := server.Start(); err != nil {
            panic(err)
        }
    })
    
    ctx.AfterScenario(func(*godog.Scenario, error) {
        server.Stop()
    })
    
    shutdown_steps.InitializeShutdownSteps(ctx, client, server)
}
```

## Debugging Techniques

### Server Health Checks

```bash
# Check if server is running
curl http://localhost:9191/api/ready

# Check health endpoint
curl http://localhost:9191/api/health

# Test greet endpoint
curl http://localhost:9191/api/v1/greet/John
```

### Common Server Issues

| Issue | Cause | Solution |
|-------|-------|----------|
| Connection refused | Server not started | Check BeforeSuite hook |
| Port already in use | Previous test crashed | Kill process on port 9191 |
| Server not ready | Startup timeout | Increase maxAttempts in waitForServerReady |
| Wrong responses | Configuration issue | Verify createTestConfig values |

### Debugging Server Startup

```go
// Add debug logging to waitForServerReady
func (s *Server) waitForServerReady() error {
    for attempt := 0; attempt < 30; attempt++ {
        log.Debug().Int("attempt", attempt+1).Msg("Checking server readiness")
        
        resp, err := http.Get(s.baseURL + "/api/ready")
        
        if err != nil {
            log.Debug().Err(err).Msg("Server not ready yet")
        } else {
            log.Debug().Int("status", resp.StatusCode).Msg("Server responded")
            resp.Body.Close()
            if resp.StatusCode == http.StatusOK {
                log.Info().Msg("Server is ready")
                return nil
            }
        }
        
        time.Sleep(100 * time.Millisecond)
    }
    
    return fmt.Errorf("server never became ready")
}
```

## Performance Optimization

### Connection Reuse

```go
// Create reusable HTTP client
var testClient = &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        10,
        IdleConnTimeout:     90 * time.Second,
        DisableKeepAlives:   false,
        DisableCompression:  true,
    },
}

// Use in client requests
resp, err := testClient.Do(req)
```

### Parallel Test Execution

```go
// pkg/bdd/bdd_test.go
func TestBDD(t *testing.T) {
    suite := godog.TestSuite{
        Name:                 "DanceLessonsCoach BDD Tests",
        TestSuiteInitializer: bdd.InitializeTestSuite,
        ScenarioInitializer:   bdd.InitializeScenario,
        Options: &godog.Options{
            Format:   "progress",
            Paths:    []string{"."},
            TestingT: t,
            // Enable parallel execution
            Concurrency: 4,  // Number of parallel scenarios
        },
    }
    
    if suite.Run() != 0 {
        t.Fatal("non-zero status returned, failed to run BDD tests")
    }
}
```

## Advanced Patterns

### Dynamic Port Allocation

**Not recommended** for our use case, but possible:

```go
func findFreePort() (int, error) {
    addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
    if err != nil {
        return 0, err
    }
    
    l, err := net.ListenTCP("tcp", addr)
    if err != nil {
        return 0, err
    }
    defer l.Close()
    
    return l.Addr().(*net.TCPAddr).Port, nil
}
```

### Multiple Server Instances

```go
// For testing different configurations
type ServerConfig struct {
    Port    int
    Timeout time.Duration
    Logging bool
}

func NewServerWithConfig(config ServerConfig) *Server {
    return &Server{
        port: config.Port,
        // ...
    }
}
```

### Custom Middleware

```go
// Add test-specific middleware
func (s *Server) Start() error {
    // ... existing setup ...
    
    // Add test middleware
    handler := s.httpServer.Handler
    s.httpServer.Handler = addTestMiddleware(handler)
    
    // ... rest of startup ...
}

func addTestMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Add test headers, logging, etc.
        w.Header().Set("X-Test-Server", "true")
        next.ServeHTTP(w, r)
    })
}
```

## Security Considerations

### Test Server Isolation

```go
// Bind to localhost only
func (s *Server) Start() error {
    s.httpServer.Addr = "localhost:9191"  // localhost only!
    // ...
}
```

### Sensitive Data Handling

```go
// Scrub sensitive data from test responses
func scrubSensitiveData(body []byte) []byte {
    // Remove API keys, tokens, etc.
    return bytes.ReplaceAll(body, []byte("api-key-"), []byte("REDACTED-"))
}
```

### Resource Cleanup

```go
// Ensure proper cleanup in AfterSuite
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
    ctx.AfterSuite(func() {
        if sharedServer != nil {
            // Give server time to shutdown gracefully
            ctx := context.Background()
            if err := sharedServer.Stop(); err != nil {
                log.Error().Err(err).Msg("Failed to stop test server")
            }
            
            // Verify server is actually stopped
            for i := 0; i < 5; i++ {
                _, err := http.Get("http://localhost:9191/api/health")
                if err != nil {
                    break  // Server stopped
                }
                time.Sleep(100 * time.Millisecond)
            }
        }
    })
}
```

## Best Practices Summary

### ✅ DO

1. **Use fixed port** (9191) for consistency
2. **Verify server readiness** before running tests
3. **Use real server code** for realistic testing
4. **Implement graceful shutdown** with timeouts
5. **Reuse HTTP connections** for better performance
6. **Clean up resources** in AfterSuite hooks
7. **Bind to localhost** for security
8. **Add debug logging** for troubleshooting

### ❌ DON'T

1. **Don't use external processes** (complex management)
2. **Don't mock server responses** (defeats black box testing)
3. **Don't share state between scenarios** (use fresh clients)
4. **Don't ignore shutdown errors** (resource leaks)
5. **Don't use dynamic ports** (harder to debug)
6. **Don't expose test server externally** (security risk)
7. **Don't forget to clean up** (port conflicts)

## Troubleshooting Checklist

1. **Server not starting?**
   - [ ] Check port 9191 is available
   - [ ] Verify BeforeSuite hook runs
   - [ ] Check server logs for errors
   - [ ] Test readiness endpoint manually

2. **Tests timing out?**
   - [ ] Increase waitForServerReady attempts
   - [ ] Check server startup logs
   - [ ] Verify database connections (if any)
   - [ ] Test with simpler scenarios first

3. **Connection refused?**
   - [ ] Verify server is running (`curl localhost:9191`)
   - [ ] Check for port conflicts
   - [ ] Restart test suite
   - [ ] Kill any zombie processes

4. **Wrong responses?**
   - [ ] Verify test configuration
   - [ ] Check real server implementation
   - [ ] Test endpoints manually
   - [ ] Compare with production behavior

## Performance Benchmarks

### Our Implementation Results

| Metric | Value |
|--------|-------|
| Server startup time | ~100-200ms |
| Test execution time | ~50-100ms per scenario |
| Memory usage | ~50-100MB |
| Concurrent scenarios | 4-8 parallel |
| Total test suite | ~1-2 seconds |

### Optimization Opportunities

1. **Connection pooling**: Reuse HTTP connections
2. **Parallel execution**: Run scenarios concurrently
3. **Lazy initialization**: Start server only when needed
4. **Caching**: Cache configuration and setup
5. **Minimal logging**: Reduce log overhead in tests

## Integration with Existing Code

### Using Real Server Components

```go
// pkg/bdd/testserver/server.go
func (s *Server) Start() error {
    // Use REAL server from pkg/server
    cfg := createTestConfig(s.port)
    realServer := server.NewServer(cfg, context.Background())
    
    // Use real router with all real handlers
    s.httpServer.Handler = realServer.Router()
    
    return s.waitForServerReady()
}
```

### Benefits of Real Server Integration

1. **Realistic testing**: Tests actual server behavior
2. **No mocking needed**: Uses real handlers and middleware
3. **Catches real bugs**: Finds issues that would occur in production
4. **Easy maintenance**: Changes to server automatically reflected in tests
5. **Consistent behavior**: Tests match production exactly

## Future Enhancements

### Potential Improvements

1. **Automatic port detection**: Find free port if 9191 is taken
2. **Health monitoring**: Continuous server health checks
3. **Performance metrics**: Track test execution times
4. **Test coverage**: Integration with coverage tools
5. **Docker support**: Run tests in containers
6. **Configuration options**: Make port, timeouts configurable

### Not Recommended

1. **Dynamic port allocation**: Makes debugging harder
2. **External process management**: Too complex and unreliable
3. **Mock servers**: Defeats black box testing purpose
4. **Global state sharing**: Causes test interference

## Conclusion

The hybrid in-process test server pattern provides the perfect balance of:
- **Reliability**: No external process management issues
- **Realism**: Uses actual server code and behavior
- **Performance**: Fast startup and execution
- **Debuggability**: Fixed port and clear architecture
- **Maintainability**: Simple implementation and integration

This approach has proven successful in our BDD implementation and is recommended for all API testing scenarios.