Handling forward routing rules using routing plugin

This commit is contained in:
tanyamadaan
2025-03-21 23:19:01 +05:30
parent fb1e948616
commit afcd17c125
8 changed files with 449 additions and 201 deletions

View File

@@ -18,9 +18,9 @@ func (rp RouterProvider) New(ctx context.Context, config map[string]string) (def
} }
// Parse the routing_config key from the config map // Parse the routing_config key from the config map
routingConfig, ok := config["routing_config"] routingConfig, ok := config["routingConfig"]
if !ok { if !ok {
return nil, nil, errors.New("routing_config is required in the configuration") return nil, nil, errors.New("routingConfig is required in the configuration")
} }
return router.New(ctx, &router.Config{ return router.New(ctx, &router.Config{
RoutingConfig: routingConfig, RoutingConfig: routingConfig,

View File

@@ -13,17 +13,17 @@ func setupTestConfig(t *testing.T) string {
t.Helper() t.Helper()
// Create a temporary directory for the routing rules // Create a temporary directory for the routing rules
configDir, err := os.MkdirTemp("", "routing_rules") configDir, err := os.MkdirTemp("", "routingRules")
if err != nil { if err != nil {
t.Fatalf("Failed to create temp directory: %v", err) t.Fatalf("Failed to create temp directory: %v", err)
} }
// Define sample routing rules // Define sample routing rules
rulesContent := ` rulesContent := `
routing_rules: routingRules:
- domain: "ONDC:TRV11" - domain: "ONDC:TRV11"
version: "2.0.0" version: "2.0.0"
routing_type: "url" routingType: "url"
target: target:
url: "https://services-backend/trv/v1" url: "https://services-backend/trv/v1"
endpoints: endpoints:
@@ -34,7 +34,7 @@ routing_rules:
- domain: "ONDC:TRV11" - domain: "ONDC:TRV11"
version: "2.0.0" version: "2.0.0"
routing_type: "msgq" routingType: "msgq"
target: target:
topic_id: "trv_topic_id1" topic_id: "trv_topic_id1"
endpoints: endpoints:
@@ -42,7 +42,7 @@ routing_rules:
` `
// Write the routing rules to a file // Write the routing rules to a file
rulesFilePath := filepath.Join(configDir, "routing_rules.yaml") rulesFilePath := filepath.Join(configDir, "routingRules.yaml")
if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil { if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil {
t.Fatalf("Failed to write routing rules file: %v", err) t.Fatalf("Failed to write routing rules file: %v", err)
} }
@@ -50,8 +50,8 @@ routing_rules:
return rulesFilePath return rulesFilePath
} }
// TestRouterProvider_Success tests the RouterProvider implementation for success cases. // TestRouterProviderSuccess tests the RouterProvider implementation for success cases.
func TestRouterProvider_Success(t *testing.T) { func TestRouterProviderSuccess(t *testing.T) {
rulesFilePath := setupTestConfig(t) rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath)) defer os.RemoveAll(filepath.Dir(rulesFilePath))
@@ -65,7 +65,7 @@ func TestRouterProvider_Success(t *testing.T) {
name: "Valid configuration", name: "Valid configuration",
ctx: context.Background(), ctx: context.Background(),
config: map[string]string{ config: map[string]string{
"routing_config": rulesFilePath, "routingConfig": rulesFilePath,
}, },
}, },
} }
@@ -73,7 +73,7 @@ func TestRouterProvider_Success(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
provider := RouterProvider{} provider := RouterProvider{}
router, closeFunc, err := provider.New(tt.ctx, tt.config) router, _, err := provider.New(tt.ctx, tt.config)
// Ensure no error occurred // Ensure no error occurred
if err != nil { if err != nil {
@@ -85,20 +85,12 @@ func TestRouterProvider_Success(t *testing.T) {
if router == nil { if router == nil {
t.Error("expected a non-nil Router instance, got nil") t.Error("expected a non-nil Router instance, got nil")
} }
if closeFunc == nil {
t.Error("expected a non-nil close function, got nil")
}
// Test the close function
if err := closeFunc(); err != nil {
t.Errorf("close function returned an error: %v", err)
}
}) })
} }
} }
// TestRouterProvider_Failure tests the RouterProvider implementation for failure cases. // TestRouterProviderFailure tests the RouterProvider implementation for failure cases.
func TestRouterProvider_Failure(t *testing.T) { func TestRouterProviderFailure(t *testing.T) {
rulesFilePath := setupTestConfig(t) rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath)) defer os.RemoveAll(filepath.Dir(rulesFilePath))
@@ -113,20 +105,20 @@ func TestRouterProvider_Failure(t *testing.T) {
name: "Empty routing config path", name: "Empty routing config path",
ctx: context.Background(), ctx: context.Background(),
config: map[string]string{ config: map[string]string{
"routing_config": "", "routingConfig": "",
}, },
expectedError: "failed to load routing rules: routing_config path is empty", expectedError: "failed to load routing rules: routingConfig path is empty",
}, },
{ {
name: "Missing routing config key", name: "Missing routing config key",
ctx: context.Background(), ctx: context.Background(),
config: map[string]string{}, config: map[string]string{},
expectedError: "routing_config is required in the configuration", expectedError: "routingConfig is required in the configuration",
}, },
{ {
name: "Nil context", name: "Nil context",
ctx: nil, ctx: nil,
config: map[string]string{"routing_config": rulesFilePath}, config: map[string]string{"routingConfig": rulesFilePath},
expectedError: "context cannot be nil", expectedError: "context cannot be nil",
}, },
} }

View File

@@ -16,12 +16,12 @@ import (
// Config holds the configuration for the Router plugin. // Config holds the configuration for the Router plugin.
type Config struct { type Config struct {
RoutingConfig string `json:"routing_config"` RoutingConfig string `json:"routingConfig"`
} }
// RoutingConfig represents the structure of the routing configuration file. // RoutingConfig represents the structure of the routing configuration file.
type routingConfig struct { type routingConfig struct {
RoutingRules []routingRule `yaml:"routing_rules"` RoutingRules []routingRule `yaml:"routingRules"`
} }
// Router implements Router interface // Router implements Router interface
@@ -34,18 +34,20 @@ type Router struct {
type routingRule struct { type routingRule struct {
Domain string `yaml:"domain"` Domain string `yaml:"domain"`
Version string `yaml:"version"` Version string `yaml:"version"`
RoutingType string `yaml:"routing_type"` // "url" or "msgq" RoutingType string `yaml:"routingType"` // "url", "msgq", "bpp", or "bap"
Target target `yaml:"target"` Target target `yaml:"target,omitempty"`
Endpoints []string `yaml:"endpoints"` Endpoints []string `yaml:"endpoints"`
} }
// Target contains destination-specific details. // Target contains destination-specific details.
type target struct { type target struct {
URL string `yaml:"url,omitempty"` // For "url" type URL string `yaml:"url,omitempty"` // URL for "url" or gateway endpoint for "bpp"/"bap"
TopicID string `yaml:"topic_id,omitempty"` // For "msgq" type TopicID string `yaml:"topic_id,omitempty"` // For "msgq" type
} }
// New initializes a new ProxyRouter instance. // New initializes a new Router instance with the provided configuration.
// It loads and validates the routing rules from the specified YAML file.
// Returns an error if the configuration is invalid or the rules cannot be loaded.
func New(ctx context.Context, config *Config) (*Router, func() error, error) { func New(ctx context.Context, config *Config) (*Router, func() error, error) {
// Check if config is nil // Check if config is nil
if config == nil { if config == nil {
@@ -59,17 +61,17 @@ func New(ctx context.Context, config *Config) (*Router, func() error, error) {
if err := router.loadRules(); err != nil { if err := router.loadRules(); err != nil {
return nil, nil, fmt.Errorf("failed to load routing rules: %w", err) return nil, nil, fmt.Errorf("failed to load routing rules: %w", err)
} }
return router, router.Close, nil return router, nil, nil
} }
// LoadRules reads and parses routing rules from the YAML configuration file. // LoadRules reads and parses routing rules from the YAML configuration file.
func (r *Router) loadRules() error { func (r *Router) loadRules() error {
if r.config.RoutingConfig == "" { if r.config.RoutingConfig == "" {
return fmt.Errorf("routing_config path is empty") return fmt.Errorf("routingConfig path is empty")
} }
data, err := os.ReadFile(r.config.RoutingConfig) data, err := os.ReadFile(r.config.RoutingConfig)
if err != nil { if err != nil {
return fmt.Errorf("error reading config file: %w", err) return fmt.Errorf("error reading config file at %s: %w", r.config.RoutingConfig, err)
} }
var config routingConfig var config routingConfig
if err := yaml.Unmarshal(data, &config); err != nil { if err := yaml.Unmarshal(data, &config); err != nil {
@@ -87,21 +89,29 @@ func (r *Router) loadRules() error {
// validateRules performs basic validation on the loaded routing rules. // validateRules performs basic validation on the loaded routing rules.
func validateRules(rules []routingRule) error { func validateRules(rules []routingRule) error {
for _, rule := range rules { for _, rule := range rules {
// Ensure domain, version, and routingType are present
if rule.Domain == "" || rule.Version == "" || rule.RoutingType == "" { if rule.Domain == "" || rule.Version == "" || rule.RoutingType == "" {
return fmt.Errorf("invalid rule: domain, version, and routing_type are required") return fmt.Errorf("invalid rule: domain, version, and routingType are required")
} }
// Validate based on routingType
switch rule.RoutingType { switch rule.RoutingType {
case "url": case "url":
if rule.Target.URL == "" { if rule.Target.URL == "" {
return fmt.Errorf("invalid rule: url is required for routing_type 'url'") return fmt.Errorf("invalid rule: url is required for routingType 'url'")
}
if _, err := url.ParseRequestURI(rule.Target.URL); err != nil {
return fmt.Errorf("invalid URL in rule: %w", err)
} }
case "msgq": case "msgq":
if rule.Target.TopicID == "" { if rule.Target.TopicID == "" {
return fmt.Errorf("invalid rule: topic_id is required for routing_type 'msgq'") return fmt.Errorf("invalid rule: topicId is required for routingType 'msgq'")
} }
case "bpp", "bap":
// No target validation needed for bpp/bap, as they use URIs from the request body
continue
default: default:
return fmt.Errorf("invalid rule: unknown routing_type '%s'", rule.RoutingType) return fmt.Errorf("invalid rule: unknown routingType '%s'", rule.RoutingType)
} }
} }
return nil return nil
@@ -115,37 +125,80 @@ func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*definit
Context struct { Context struct {
Domain string `json:"domain"` Domain string `json:"domain"`
Version string `json:"version"` Version string `json:"version"`
BppURI string `json:"bpp_uri,omitempty"`
BapURI string `json:"bap_uri,omitempty"`
} `json:"context"` } `json:"context"`
} }
if err := json.Unmarshal(body, &requestBody); err != nil { if err := json.Unmarshal(body, &requestBody); err != nil {
return nil, fmt.Errorf("error parsing request body: %w", err) return nil, fmt.Errorf("error parsing request body: %w", err)
} }
// Extract the endpoint from the URL
endpoint := path.Base(url.Path)
// Collect all matching rules for the domain and version
matchingRules := r.getMatchingRules(requestBody.Context.Domain, requestBody.Context.Version)
// If no matching rules are found, return an error
if len(matchingRules) == 0 {
return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version)
}
// Match the rule // Match the rule
for _, rule := range r.rules { for _, rule := range matchingRules {
if rule.Domain == requestBody.Context.Domain && rule.Version == requestBody.Context.Version { for _, ep := range rule.Endpoints {
// Check if the endpoint matches if strings.EqualFold(ep, endpoint) {
endpoint := path.Base(url.Path) switch rule.RoutingType {
for _, ep := range rule.Endpoints { case "msgq":
if strings.EqualFold(ep, endpoint) {
return &definition.Route{ return &definition.Route{
RoutingType: rule.RoutingType, RoutingType: rule.RoutingType,
TopicID: rule.Target.TopicID, TopicID: rule.Target.TopicID,
}, nil
case "url":
return &definition.Route{
RoutingType: rule.RoutingType,
TargetURL: rule.Target.URL, TargetURL: rule.Target.URL,
}, nil }, nil
case "bpp":
return handleRouting(rule, requestBody.Context.BppURI, endpoint, "bpp")
case "bap":
return handleRouting(rule, requestBody.Context.BapURI, endpoint, "bap")
default:
return nil, fmt.Errorf("unsupported routingType: %s", rule.RoutingType)
} }
} }
// If domain and version match but endpoint is not found, return an error
return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s", endpoint, requestBody.Context.Domain, requestBody.Context.Version)
} }
} }
// return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version) // If domain and version match but endpoint is not found, return an error
return nil, fmt.Errorf("no matching routing rule found for domain %s and version %s", requestBody.Context.Domain, requestBody.Context.Version) return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s", endpoint, requestBody.Context.Domain, requestBody.Context.Version)
} }
// Close releases resources (mock implementation returning nil). // getMatchingRules returns all rules that match the given domain and version
func (r *Router) Close() error { func (r *Router) getMatchingRules(domain, version string) []routingRule {
return nil var matchingRules []routingRule
for _, rule := range r.rules {
if rule.Domain == domain && rule.Version == version {
matchingRules = append(matchingRules, rule)
}
}
return matchingRules
}
// handleRouting handles routing for bap and bpp routing type
func handleRouting(rule routingRule, uri, endpoint string, routingType string) (*definition.Route, error) {
if uri == "" {
if rule.Target.URL != "" {
return &definition.Route{
RoutingType: routingType,
TargetURL: rule.Target.URL,
}, nil
} else {
return nil, fmt.Errorf("no target URI or URL found for %s routing type and %s endpoint", routingType, endpoint)
}
}
return &definition.Route{
RoutingType: routingType,
TargetURL: uri,
}, nil
} }

View File

@@ -2,6 +2,7 @@ package router
import ( import (
"context" "context"
"embed"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@@ -9,8 +10,10 @@ import (
"testing" "testing"
) )
// setupTestConfig creates a temporary directory and writes a sample routing rules file. //go:embed testData/*
func setupTestConfig(t *testing.T) string { var testData embed.FS
func setupTestConfig(t *testing.T, yamlFileName string) string {
t.Helper() t.Helper()
// Create a temporary directory for the routing rules // Create a temporary directory for the routing rules
@@ -19,114 +22,124 @@ func setupTestConfig(t *testing.T) string {
t.Fatalf("Failed to create temp directory: %v", err) t.Fatalf("Failed to create temp directory: %v", err)
} }
// Define sample routing rules // Read the YAML file content
rulesContent := ` yamlContent := readYAMLFile(t, yamlFileName)
routing_rules:
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- select
- init
- confirm
- status
- domain: "ONDC:TRV11"
version: "2.0.0"
routing_type: "msgq"
target:
topic_id: "trv_topic_id1"
endpoints:
- search
`
// Write the routing rules to a file // Write the routing rules to a file
rulesFilePath := filepath.Join(configDir, "routing_rules.yaml") rulesFilePath := filepath.Join(configDir, "routing_rules.yaml")
if err := os.WriteFile(rulesFilePath, []byte(rulesContent), 0644); err != nil { if err := os.WriteFile(rulesFilePath, []byte(yamlContent), 0644); err != nil {
t.Fatalf("Failed to write routing rules file: %v", err) t.Fatalf("Failed to write routing rules file: %v", err)
} }
return rulesFilePath return rulesFilePath
} }
func readYAMLFile(t *testing.T, fileName string) string {
t.Helper()
// Read the YAML file
content, err := testData.ReadFile("testData/" + fileName)
if err != nil {
t.Fatalf("Failed to read YAML file: %v", err)
}
return string(content)
}
// setupRouter is a helper function to create router instance.
func setupRouter(t *testing.T, configFile string) (*Router, func() error, string) {
rulesFilePath := setupTestConfig(t, configFile)
config := &Config{
RoutingConfig: rulesFilePath,
}
router, _, err := New(context.Background(), config)
if err != nil {
t.Fatalf("New failed: %v", err)
}
return router, nil, rulesFilePath
}
// TestNew tests the New function. // TestNew tests the New function.
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
ctx := context.Background() ctx := context.Background()
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
// Define test cases // List of YAML files in the testData directory
tests := []struct { yamlFiles := []string{
name string "bap_caller.yaml",
config *Config "bap_receiver.yaml",
expectedError string "bpp_caller.yaml",
}{ "bpp_receiver.yaml",
{
name: "Valid configuration",
config: &Config{
RoutingConfig: rulesFilePath,
},
expectedError: "",
},
{
name: "Empty config",
config: nil,
expectedError: "config cannot be nil",
},
{
name: "Empty routing config path",
config: &Config{
RoutingConfig: "",
},
expectedError: "routing_config path is empty",
},
{
name: "Routing config file does not exist",
config: &Config{
RoutingConfig: "/nonexistent/path/to/rules.yaml",
},
expectedError: "error reading config file",
},
} }
for _, tt := range tests { for _, yamlFile := range yamlFiles {
t.Run(tt.name, func(t *testing.T) { t.Run(yamlFile, func(t *testing.T) {
router, closeFunc, err := New(ctx, tt.config) rulesFilePath := setupTestConfig(t, yamlFile)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
// Check for expected error // Define test cases
if tt.expectedError != "" { tests := []struct {
if err == nil || !strings.Contains(err.Error(), tt.expectedError) { name string
t.Errorf("expected error %q, got %v", tt.expectedError, err) config *Config
} expectedError string
return }{
{
name: "Valid configuration",
config: &Config{
RoutingConfig: rulesFilePath,
},
expectedError: "",
},
{
name: "Empty config",
config: nil,
expectedError: "config cannot be nil",
},
{
name: "Empty routing config path",
config: &Config{
RoutingConfig: "",
},
expectedError: "routingConfig path is empty",
},
{
name: "Routing config file does not exist",
config: &Config{
RoutingConfig: "/nonexistent/path/to/rules.yaml",
},
expectedError: "error reading config file",
},
} }
// Ensure no error occurred for _, tt := range tests {
if err != nil { t.Run(tt.name, func(t *testing.T) {
t.Errorf("unexpected error: %v", err) router, _, err := New(ctx, tt.config)
return
}
// Ensure the router and close function are not nil // Check for expected error
if router == nil { if tt.expectedError != "" {
t.Error("expected a non-nil Router instance, got nil") if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
} t.Errorf("expected error %q, got %v", tt.expectedError, err)
if closeFunc == nil { }
t.Error("expected a non-nil close function, got nil") return
} }
// Test the close function // Ensure no error occurred
if err := closeFunc(); err != nil { if err != nil {
t.Errorf("close function returned an error: %v", err) t.Errorf("unexpected error: %v", err)
return
}
// Ensure the router and close function are not nil
if router == nil {
t.Error("expected a non-nil Router instance, got nil")
}
})
} }
}) })
} }
} }
// TestValidateRules_Success tests the validate function for success cases. // TestValidateRulesSuccess tests the validate function for success cases.
func TestValidateRules_Success(t *testing.T) { func TestValidateRulesSuccess(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
rules []routingRule rules []routingRule
@@ -135,13 +148,13 @@ func TestValidateRules_Success(t *testing.T) {
name: "Valid rules with url routing", name: "Valid rules with url routing",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
RoutingType: "url", RoutingType: "url",
Target: target{ Target: target{
URL: "https://example.com/api", URL: "https://example.com/api",
}, },
Endpoints: []string{"search", "select"}, Endpoints: []string{"on_search", "on_select"},
}, },
}, },
}, },
@@ -149,13 +162,49 @@ func TestValidateRules_Success(t *testing.T) {
name: "Valid rules with msgq routing", name: "Valid rules with msgq routing",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
RoutingType: "msgq", RoutingType: "msgq",
Target: target{ Target: target{
TopicID: "example_topic", TopicID: "example_topic",
}, },
Endpoints: []string{"search", "select"}, Endpoints: []string{"on_search", "on_select"},
},
},
},
{
name: "Valid rules with bpp routing to gateway",
rules: []routingRule{
{
Domain: "retail",
Version: "1.0.0",
RoutingType: "bpp",
Target: target{
URL: "https://mock_gateway.com/api",
},
Endpoints: []string{"search"},
},
},
},
{
name: "Valid rules with bpp routing",
rules: []routingRule{
{
Domain: "retail",
Version: "1.0.0",
RoutingType: "bpp",
Endpoints: []string{"select"},
},
},
},
{
name: "Valid rules with bap routing",
rules: []routingRule{
{
Domain: "retail",
Version: "1.0.0",
RoutingType: "bap",
Endpoints: []string{"select"},
}, },
}, },
}, },
@@ -171,8 +220,8 @@ func TestValidateRules_Success(t *testing.T) {
} }
} }
// TestValidateRules_Failure tests the validate function for failure cases. // TestValidateRulesFailure tests the validate function for failure cases.
func TestValidateRules_Failure(t *testing.T) { func TestValidateRulesFailure(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
rules []routingRule rules []routingRule
@@ -190,13 +239,13 @@ func TestValidateRules_Failure(t *testing.T) {
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: domain, version, and routing_type are required", expectedErr: "invalid rule: domain, version, and routingType are required",
}, },
{ {
name: "Missing version", name: "Missing version",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
RoutingType: "url", RoutingType: "url",
Target: target{ Target: target{
URL: "https://example.com/api", URL: "https://example.com/api",
@@ -204,13 +253,13 @@ func TestValidateRules_Failure(t *testing.T) {
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: domain, version, and routing_type are required", expectedErr: "invalid rule: domain, version, and routingType are required",
}, },
{ {
name: "Missing routing_type", name: "Missing routingType",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
Target: target{ Target: target{
URL: "https://example.com/api", URL: "https://example.com/api",
@@ -218,28 +267,28 @@ func TestValidateRules_Failure(t *testing.T) {
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: domain, version, and routing_type are required", expectedErr: "invalid rule: domain, version, and routingType are required",
}, },
{ {
name: "Invalid routing_type", name: "Invalid routingType",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
RoutingType: "invalid_type", RoutingType: "invalid",
Target: target{ Target: target{
URL: "https://example.com/api", URL: "https://example.com/api",
}, },
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: unknown routing_type 'invalid_type'", expectedErr: "invalid rule: unknown routingType 'invalid'",
}, },
{ {
name: "Missing url for routing_type: url", name: "Missing url for routingType: url",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
RoutingType: "url", RoutingType: "url",
Target: target{ Target: target{
@@ -248,13 +297,13 @@ func TestValidateRules_Failure(t *testing.T) {
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: url is required for routing_type 'url'", expectedErr: "invalid rule: url is required for routingType 'url'",
}, },
{ {
name: "Missing topic_id for routing_type: msgq", name: "Missing topic_id for routingType: msgq",
rules: []routingRule{ rules: []routingRule{
{ {
Domain: "example.com", Domain: "retail",
Version: "1.0.0", Version: "1.0.0",
RoutingType: "msgq", RoutingType: "msgq",
Target: target{ Target: target{
@@ -263,7 +312,7 @@ func TestValidateRules_Failure(t *testing.T) {
Endpoints: []string{"search", "select"}, Endpoints: []string{"search", "select"},
}, },
}, },
expectedErr: "invalid rule: topic_id is required for routing_type 'msgq'", expectedErr: "invalid rule: topicId is required for routingType 'msgq'",
}, },
} }
@@ -279,65 +328,63 @@ func TestValidateRules_Failure(t *testing.T) {
} }
} }
// TestRoute tests the Route function. // TestRouteSuccess tests the Route function for success cases.
func TestRoute(t *testing.T) { func TestRouteSuccess(t *testing.T) {
ctx := context.Background() ctx := context.Background()
rulesFilePath := setupTestConfig(t)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
config := &Config{ // Define success test cases
RoutingConfig: rulesFilePath,
}
router, closeFunc, err := New(ctx, config)
if err != nil {
t.Fatalf("New failed: %v", err)
}
defer func() {
if err := closeFunc(); err != nil {
t.Errorf("closeFunc failed: %v", err)
}
}()
// Define test cases
tests := []struct { tests := []struct {
name string name string
url string configFile string
body string url string
expectedError string body string
}{ }{
{ {
name: "Valid domain, version, and endpoint", name: "Valid domain, version, and endpoint (bpp routing with gateway URL)",
url: "https://example.com/v1/ondc/select", configFile: "bap_caller.yaml",
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`, url: "https://example.com/v1/ondc/search",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
}, },
{ {
name: "Unsupported endpoint", name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)",
url: "https://example.com/v1/ondc/unsupported", configFile: "bap_caller.yaml",
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`, url: "https://example.com/v1/ondc/select",
expectedError: "endpoint 'unsupported' is not supported for domain ONDC:TRV11 and version 2.0.0", body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`,
}, },
{ {
name: "No matching rule", name: "Valid domain, version, and endpoint (url routing)",
url: "https://example.com/v1/ondc/select", configFile: "bpp_receiver.yaml",
body: `{"context": {"domain": "ONDC:SRV11", "version": "2.0.0"}}`, url: "https://example.com/v1/ondc/select",
expectedError: "no matching routing rule found for domain ONDC:SRV11 and version 2.0.0", body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
},
{
name: "Valid domain, version, and endpoint (msgq routing)",
configFile: "bpp_receiver.yaml",
url: "https://example.com/v1/ondc/search",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
},
{
name: "Valid domain, version, and endpoint (bap routing with bap_uri)",
configFile: "bpp_caller.yaml",
url: "https://example.com/v1/ondc/on_select",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bap_uri": "https://bap1.example.com"}}`,
},
{
name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)",
configFile: "bap_receiver.yaml",
url: "https://example.com/v1/ondc/on_select",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
router, _, rulesFilePath := setupRouter(t, tt.configFile)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
parsedURL, _ := url.Parse(tt.url) parsedURL, _ := url.Parse(tt.url)
_, err := router.Route(ctx, parsedURL, []byte(tt.body)) _, err := router.Route(ctx, parsedURL, []byte(tt.body))
// Check for expected error
if tt.expectedError != "" {
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("expected error %q, got %v", tt.expectedError, err)
}
return
}
// Ensure no error occurred // Ensure no error occurred
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
@@ -345,3 +392,61 @@ func TestRoute(t *testing.T) {
}) })
} }
} }
// TestRouteFailure tests the Route function for failure cases.
func TestRouteFailure(t *testing.T) {
ctx := context.Background()
// Define failure test cases
tests := []struct {
name string
configFile string
url string
body string
expectedError string
}{
{
name: "Unsupported endpoint",
configFile: "bpp_receiver.yaml",
url: "https://example.com/v1/ondc/unsupported",
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`,
expectedError: "endpoint 'unsupported' is not supported for domain ONDC:TRV11 and version 2.0.0",
},
{
name: "No matching rule",
configFile: "bpp_receiver.yaml",
url: "https://example.com/v1/ondc/select",
body: `{"context": {"domain": "ONDC:SRV11", "version": "2.0.0"}}`,
expectedError: "no matching routing rule found for domain ONDC:SRV11 and version 2.0.0",
},
{
name: "Missing bap_uri for bap routing",
configFile: "bpp_caller.yaml",
url: "https://example.com/v1/ondc/on_search",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
expectedError: "no target URI or URL found for bap routing type and on_search endpoint",
},
{
name: "Missing bpp_uri for bpp routing",
configFile: "bap_caller.yaml",
url: "https://example.com/v1/ondc/select",
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
expectedError: "no target URI or URL found for bpp routing type and select endpoint",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router, _, rulesFilePath := setupRouter(t, tt.configFile)
defer os.RemoveAll(filepath.Dir(rulesFilePath))
parsedURL, _ := url.Parse(tt.url)
_, err := router.Route(ctx, parsedURL, []byte(tt.body))
// Check for expected error
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
t.Errorf("expected error %q, got %v", tt.expectedError, err)
}
})
}
}

View File

@@ -0,0 +1,25 @@
routingRules:
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "bpp"
target:
url: "https://gateway.example.com"
endpoints:
- search
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "bpp"
endpoints:
- select
- init
- confirm
- status
- cancel
- domain: "ONDC:TRV12"
version: "2.0.0"
routingType: "bpp"
endpoints:
- select
- init
- confirm
- status

View File

@@ -0,0 +1,20 @@
routingRules:
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- on_select
- on_init
- on_confirm
- on_status
- on_update
- on_cancel
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "msgq"
target:
topic_id: "trv_topic_id1"
endpoints:
- on_search

View File

@@ -0,0 +1,23 @@
routingRules:
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "bap"
endpoints:
- on_search
- on_select
- on_init
- on_confirm
- on_status
- on_update
- on_cancel
- domain: "ONDC:TRV11"
version: "2.0.0"
routingType: "bap"
endpoints:
- on_search
- on_select
- on_init
- on_confirm
- on_status
- on_update
- on_cancel

View File

@@ -0,0 +1,30 @@
routingRules:
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- select
- init
- confirm
- status
- cancel
- domain: "ONDC:TRV10"
version: "2.0.0"
routingType: "msgq"
target:
topic_id: "trv_topic_id1"
endpoints:
- search
- domain: "ONDC:TRV11"
version: "2.0.0"
routingType: "url"
target:
url: "https://services-backend/trv/v1"
endpoints:
- select
- init
- confirm