✨ feat: implement API v2 with feature flag control
- Added /api/v2/greet POST endpoint with JSON request/response - Implemented ServiceV2 with Hello my friend <name>! greeting format - Added api.v2_enabled feature flag (default: false) - Extended BDD tests to cover v2 scenarios - Maintained full backward compatibility with v1 API - Added DLC_API_V2_ENABLED environment variable support - Created ADR 0010-api-v2-feature-flag.md - Updated configuration system to support API versioning
This commit is contained in:
@@ -27,6 +27,8 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client) {
|
||||
ctx.Step(`^I request the health endpoint$`, sc.iRequestTheHealthEndpoint)
|
||||
ctx.Step(`^the response should be "{\\"([^"]*)":\\"([^"]*)"}"$`, sc.theResponseShouldBe)
|
||||
ctx.Step(`^the server is running$`, sc.theServerIsRunning)
|
||||
ctx.Step(`^the server is running with v2 enabled$`, sc.theServerIsRunningWithV2Enabled)
|
||||
ctx.Step(`^I send a POST request to v2 greet with name "([^"]*)"$`, sc.iSendPOSTRequestToV2GreetWithName)
|
||||
}
|
||||
|
||||
func (sc *StepContext) iRequestAGreetingFor(name string) error {
|
||||
@@ -59,3 +61,33 @@ func (sc *StepContext) theServerIsRunning() error {
|
||||
// Actually verify the server is running by checking the readiness endpoint
|
||||
return sc.client.Request("GET", "/api/ready", nil)
|
||||
}
|
||||
|
||||
func (sc *StepContext) theServerIsRunningWithV2Enabled() error {
|
||||
// Verify the server is running and v2 is enabled by checking v2 endpoint exists
|
||||
// First check server is running
|
||||
if err := sc.client.Request("GET", "/api/ready", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if v2 endpoint is available (should return 405 Method Not Allowed for GET, which means endpoint exists)
|
||||
// If v2 is disabled, this will return 404
|
||||
resp, err := sc.client.CustomRequest("GET", "/api/v2/greet", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// If we get 405, v2 is enabled (endpoint exists but doesn't allow GET)
|
||||
// If we get 404, v2 is disabled
|
||||
if resp.StatusCode == 404 {
|
||||
return fmt.Errorf("v2 endpoint not available - v2 feature flag not enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *StepContext) iSendPOSTRequestToV2GreetWithName(name string) error {
|
||||
// Create JSON request body
|
||||
requestBody := map[string]string{"name": name}
|
||||
return sc.client.Request("POST", "/api/v2/greet", requestBody)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package testserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -19,12 +21,37 @@ func NewClient(server *Server) *Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Request(method, path string, body []byte) error {
|
||||
func (c *Client) Request(method, path string, body interface{}) error {
|
||||
url := c.server.GetBaseURL() + path
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
// Handle different body types
|
||||
switch b := body.(type) {
|
||||
case []byte:
|
||||
reqBody = bytes.NewReader(b)
|
||||
case string:
|
||||
reqBody = strings.NewReader(b)
|
||||
case map[string]string:
|
||||
jsonBody, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(jsonBody)
|
||||
default:
|
||||
return fmt.Errorf("unsupported body type: %T", body)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set content type for JSON bodies
|
||||
if body != nil && reqBody != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -41,6 +68,53 @@ func (c *Client) Request(method, path string, body []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CustomRequest(method, path string, body interface{}) (*http.Response, error) {
|
||||
url := c.server.GetBaseURL() + path
|
||||
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
// Handle different body types
|
||||
switch b := body.(type) {
|
||||
case []byte:
|
||||
reqBody = bytes.NewReader(b)
|
||||
case string:
|
||||
reqBody = strings.NewReader(b)
|
||||
case map[string]string:
|
||||
jsonBody, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal JSON body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(jsonBody)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported body type: %T", body)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set content type for JSON bodies
|
||||
if body != nil && reqBody != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
// Don't close the body here - let the caller handle it
|
||||
c.lastResp = resp
|
||||
c.lastBody, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) ExpectResponseBody(expected string) error {
|
||||
if c.lastResp == nil {
|
||||
return fmt.Errorf("no response received")
|
||||
|
||||
@@ -100,5 +100,8 @@ func createTestConfig(port int) *config.Config {
|
||||
Telemetry: config.TelemetryConfig{
|
||||
Enabled: false,
|
||||
},
|
||||
API: config.APIConfig{
|
||||
V2Enabled: true, // Enable v2 for testing
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user