Changes to Response Error struct

This commit is contained in:
tanyamadaan
2025-03-19 13:54:23 +05:30
22 changed files with 1532 additions and 84 deletions

View File

@@ -5,10 +5,10 @@ import (
"errors"
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
validator "github.com/beckn/beckn-onix/pkg/plugin/implementation/schemaValidator"
schemaValidator "github.com/beckn/beckn-onix/pkg/plugin/implementation/schemaValidator"
)
// ValidatorProvider provides instance of Validator.
// schemaValidatorProvider provides instances of schemaValidator.
type schemaValidatorProvider struct{}
// New initializes a new Verifier instance.
@@ -23,11 +23,11 @@ func (vp schemaValidatorProvider) New(ctx context.Context, config map[string]str
return nil, nil, errors.New("config must contain 'schema_dir'")
}
// Create a new Validator instance with the provided configuration
return validator.New(ctx, &validator.Config{
SchemaDir: schemaDir, // Pass the schemaDir to the validator.Config
// Create a new schemaValidator instance with the provided configuration
return schemaValidator.New(ctx, &schemaValidator.Config{
SchemaDir: schemaDir,
})
}
// Provider is the exported symbol that the plugin manager will look for.
var Provider definition.SchemaValidatorProvider = &schemaValidatorProvider{}
var Provider definition.SchemaValidatorProvider = schemaValidatorProvider{}

View File

@@ -72,7 +72,7 @@ func TestValidatorProviderSuccess(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vp := schemaValidatorProvider{}
validator, close, err := vp.New(tt.ctx, tt.config)
schemaValidator, close, err := vp.New(tt.ctx, tt.config)
// Ensure no error occurred
if err != nil {
@@ -80,9 +80,9 @@ func TestValidatorProviderSuccess(t *testing.T) {
return
}
// Ensure the validator is not nil
if validator == nil {
t.Error("expected a non-nil validator, got nil")
// Ensure the schemaValidator is not nil
if schemaValidator == nil {
t.Error("expected a non-nil schemaValidator, got nil")
}
// Ensure the close function is not nil
@@ -110,11 +110,23 @@ func TestValidatorProviderFailure(t *testing.T) {
config map[string]string
expectedError string
}{
{
name: "Config is empty",
ctx: context.Background(),
config: map[string]string{},
expectedError: "config must contain 'schema_dir'",
},
{
name: "schema_dir is empty",
ctx: context.Background(),
config: map[string]string{"schema_dir": ""},
expectedError: "config must contain 'schema_dir'",
},
{
name: "Invalid schema directory",
ctx: context.Background(), // Valid context
config: map[string]string{"schema_dir": "/invalid/dir"},
expectedError: "failed to initialise validator: schema directory does not exist: /invalid/dir",
expectedError: "failed to initialise schemaValidator: schema directory does not exist: /invalid/dir",
},
{
name: "Nil context",

View File

@@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
response "github.com/beckn/beckn-onix/pkg/response"
"github.com/santhosh-tekuri/jsonschema/v6"
)
@@ -23,12 +23,13 @@ type payload struct {
} `json:"context"`
}
// Validator implements the Validator interface.
// SchemaValidator implements the Validator interface.
type SchemaValidator struct {
config *Config
schemaCache map[string]*jsonschema.Schema
}
// Config struct for SchemaValidator.
type Config struct {
SchemaDir string
}
@@ -45,8 +46,8 @@ func New(ctx context.Context, config *Config) (*SchemaValidator, func() error, e
}
// Call Initialise function to load schemas and get validators
if err := v.Initialise(); err != nil {
return nil, nil, fmt.Errorf("failed to initialise validator: %v", err)
if err := v.initialise(); err != nil {
return nil, nil, fmt.Errorf("failed to initialise schemaValidator: %v", err)
}
return v, v.Close, nil
}
@@ -88,20 +89,20 @@ func (v *SchemaValidator) Validate(ctx context.Context, url *url.URL, data []byt
// Handle schema validation errors
if validationErr, ok := err.(*jsonschema.ValidationError); ok {
// Convert validation errors into an array of SchemaValError
var schemaErrors []definition.SchemaValError
var schemaErrors []response.Error
for _, cause := range validationErr.Causes {
// Extract the path and message from the validation error
path := strings.Join(cause.InstanceLocation, ".") // JSON path to the invalid field
message := cause.Error() // Validation error message
// Append the error to the schemaErrors array
schemaErrors = append(schemaErrors, definition.SchemaValError{
Path: path,
schemaErrors = append(schemaErrors, response.Error{
Paths: path,
Message: message,
})
}
// Return the array of schema validation errors
return &definition.SchemaValidationErr{Errors: schemaErrors}
return &response.SchemaValidationErr{Errors: schemaErrors}
}
// Return a generic error for non-validation errors
return fmt.Errorf("validation failed: %v", err)
@@ -116,7 +117,7 @@ type ValidatorProvider struct{}
// Initialise initialises the validator provider by compiling all the JSON schema files
// from the specified directory and storing them in a cache indexed by their schema filenames.
func (v *SchemaValidator) Initialise() error {
func (v *SchemaValidator) initialise() error {
schemaDir := v.config.SchemaDir
// Check if the directory exists and is accessible.
info, err := os.Stat(schemaDir)

View File

@@ -8,7 +8,7 @@ import (
"strings"
"testing"
"github.com/beckn/beckn-onix/pkg/plugin/definition"
"github.com/santhosh-tekuri/jsonschema/v6"
)
// setupTestSchema creates a temporary directory and writes a sample schema file.
@@ -54,16 +54,16 @@ func setupTestSchema(t *testing.T) string {
func TestValidator_Validate_Success(t *testing.T) {
tests := []struct {
name string
url string
payload string
wantValid bool
name string
url string
payload string
wantErr bool
}{
{
name: "Valid payload",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0", "action": "endpoint"}}`,
wantValid: true,
name: "Valid payload",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0", "action": "endpoint"}}`,
wantErr: false,
},
}
@@ -80,12 +80,11 @@ func TestValidator_Validate_Success(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, _ := url.Parse(tt.url)
valid, err := v.Validate(context.Background(), u, []byte(tt.payload))
if err != (definition.SchemaValError{}) {
err := v.Validate(context.Background(), u, []byte(tt.payload))
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if valid != tt.wantValid {
t.Errorf("Error: Validate() returned valid = %v, expected valid = %v", valid, tt.wantValid)
} else {
t.Logf("Test %s passed with no errors", tt.name)
}
})
}
@@ -93,32 +92,28 @@ func TestValidator_Validate_Success(t *testing.T) {
func TestValidator_Validate_Failure(t *testing.T) {
tests := []struct {
name string
url string
payload string
wantValid bool
wantErr string
name string
url string
payload string
wantErr string
}{
{
name: "Invalid JSON payload",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"`,
wantValid: false,
wantErr: "failed to parse JSON payload",
name: "Invalid JSON payload",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"`,
wantErr: "failed to parse JSON payload",
},
{
name: "Schema validation failure",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
wantValid: false,
wantErr: "Validation failed",
name: "Schema validation failure",
url: "http://example.com/endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
wantErr: "context: at '/context': missing property 'action'",
},
{
name: "Schema not found",
url: "http://example.com/unknown_endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
wantValid: false,
wantErr: "schema not found for domain",
name: "Schema not found",
url: "http://example.com/unknown_endpoint",
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
wantErr: "schema not found for domain",
},
}
@@ -135,13 +130,21 @@ func TestValidator_Validate_Failure(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u, _ := url.Parse(tt.url)
valid, err := v.Validate(context.Background(), u, []byte(tt.payload))
if (err != (definition.SchemaValError{}) && !strings.Contains(err.Message, tt.wantErr)) || (err == (definition.SchemaValError{}) && tt.wantErr != "") {
t.Errorf("Error: Validate() returned error = %v, expected error = %v", err, tt.wantErr)
return
}
if valid != tt.wantValid {
t.Errorf("Validate() returned valid = %v, expected valid = %v", valid, tt.wantValid)
err := v.Validate(context.Background(), u, []byte(tt.payload))
if tt.wantErr != "" {
if err == nil {
t.Errorf("Expected error containing '%s', but got nil", tt.wantErr)
} else if !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("Expected error containing '%s', but got '%v'", tt.wantErr, err)
} else {
t.Logf("Test %s passed with expected error: %v", tt.name, err)
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
} else {
t.Logf("Test %s passed with no errors", tt.name)
}
}
})
}
@@ -269,11 +272,14 @@ func TestValidator_Initialise(t *testing.T) {
}
config := &Config{SchemaDir: schemaDir}
v := &Validator{config: config}
v := &SchemaValidator{
config: config,
schemaCache: make(map[string]*jsonschema.Schema),
}
err := v.Initialise()
err := v.initialise()
if (err != nil && !strings.Contains(err.Error(), tt.wantErr)) || (err == nil && tt.wantErr != "") {
t.Errorf("Error: Initialise() returned error = %v, expected error = %v", err, tt.wantErr)
t.Errorf("Error: initialise() returned error = %v, expected error = %v", err, tt.wantErr)
} else if err == nil {
t.Logf("Test %s passed: validator initialized successfully", tt.name)
} else {
@@ -309,22 +315,22 @@ func TestValidator_New_Failure(t *testing.T) {
},
wantErr: "config cannot be nil",
},
{
name: "Config is empty",
config: &Config{},
setupFunc: func(schemaDir string) error {
return nil
},
wantErr: "config must contain 'schema_dir'",
},
{
name: "schema_dir is empty",
config: &Config{SchemaDir: ""},
setupFunc: func(schemaDir string) error {
return nil
},
wantErr: "config must contain 'schema_dir'",
},
// {
// name: "Config is empty",
// config: &Config{},
// setupFunc: func(schemaDir string) error {
// return nil
// },
// wantErr: "config must contain 'schema_dir'",
// },
// {
// name: "schema_dir is empty",
// config: &Config{SchemaDir: ""},
// setupFunc: func(schemaDir string) error {
// return nil
// },
// wantErr: "config must contain 'schema_dir'",
// },
{
name: "Failed to initialise validators",
config: &Config{
@@ -334,7 +340,7 @@ func TestValidator_New_Failure(t *testing.T) {
// Do not create the schema directory
return nil
},
wantErr: "failed to initialise validator",
wantErr: "ailed to initialise schemaValidator: schema directory does not exist: /invalid/path",
},
}

143
pkg/response/response.go Normal file
View File

@@ -0,0 +1,143 @@
package response
import (
"context"
"encoding/json"
"fmt"
)
type ErrorType string
const (
SchemaValidationErrorType ErrorType = "SCHEMA_VALIDATION_ERROR"
InvalidRequestErrorType ErrorType = "INVALID_REQUEST"
)
type BecknRequest struct {
Context map[string]interface{} `json:"context,omitempty"`
}
type Error struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Paths string `json:"paths,omitempty"`
}
type Message struct {
Ack struct {
Status string `json:"status,omitempty"`
} `json:"ack,omitempty"`
Error *Error `json:"error,omitempty"`
}
type BecknResponse struct {
Context map[string]interface{} `json:"context,omitempty"`
Message Message `json:"message,omitempty"`
}
type ClientFailureBecknResponse struct {
Context map[string]interface{} `json:"context,omitempty"`
Error *Error `json:"error,omitempty"`
}
var errorMap = map[ErrorType]Error{
SchemaValidationErrorType: {
Code: "400",
Message: "Schema validation failed",
},
InvalidRequestErrorType: {
Code: "401",
Message: "Invalid request format",
},
}
var DefaultError = Error{
Code: "500",
Message: "Internal server error",
}
func Nack(ctx context.Context, tp ErrorType, paths string, body []byte) ([]byte, error) {
var req BecknRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, fmt.Errorf("failed to parse request: %w", err)
}
errorObj, ok := errorMap[tp]
if paths != "" {
errorObj.Paths = paths
}
var response BecknResponse
if !ok {
response = BecknResponse{
Context: req.Context,
Message: Message{
Ack: struct {
Status string `json:"status,omitempty"`
}{
Status: "NACK",
},
Error: &DefaultError,
},
}
} else {
response = BecknResponse{
Context: req.Context,
Message: Message{
Ack: struct {
Status string `json:"status,omitempty"`
}{
Status: "NACK",
},
Error: &errorObj,
},
}
}
return json.Marshal(response)
}
func Ack(ctx context.Context, body []byte) ([]byte, error) {
var req BecknRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, fmt.Errorf("failed to parse request: %w", err)
}
response := BecknResponse{
Context: req.Context,
Message: Message{
Ack: struct {
Status string `json:"status,omitempty"`
}{
Status: "ACK",
},
},
}
return json.Marshal(response)
}
func HandleClientFailure(ctx context.Context, tp ErrorType, body []byte) ([]byte, error) {
var req BecknRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, fmt.Errorf("failed to parse request: %w", err)
}
errorObj, ok := errorMap[tp]
var response ClientFailureBecknResponse
if !ok {
response = ClientFailureBecknResponse{
Context: req.Context,
Error: &DefaultError,
}
} else {
response = ClientFailureBecknResponse{
Context: req.Context,
Error: &errorObj,
}
}
return json.Marshal(response)
}

View File

@@ -0,0 +1,303 @@
package response
import (
"context"
"encoding/json"
"reflect"
"testing"
)
func TestNack(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
errorType ErrorType
requestBody string
wantStatus string
wantErrCode string
wantErrMsg string
wantErr bool
path string
}{
{
name: "Schema validation error",
errorType: SchemaValidationErrorType,
requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`,
wantStatus: "NACK",
wantErrCode: "400",
wantErrMsg: "Schema validation failed",
wantErr: false,
path: "test",
},
{
name: "Invalid request error",
errorType: InvalidRequestErrorType,
requestBody: `{"context": {"domain": "test-domain"}}`,
wantStatus: "NACK",
wantErrCode: "401",
wantErrMsg: "Invalid request format",
wantErr: false,
path: "test",
},
{
name: "Unknown error type",
errorType: "UNKNOWN_ERROR",
requestBody: `{"context": {"domain": "test-domain"}}`,
wantStatus: "NACK",
wantErrCode: "500",
wantErrMsg: "Internal server error",
wantErr: false,
path: "test",
},
{
name: "Empty request body",
errorType: SchemaValidationErrorType,
requestBody: `{}`,
wantStatus: "NACK",
wantErrCode: "400",
wantErrMsg: "Schema validation failed",
wantErr: false,
path: "test",
},
{
name: "Invalid JSON",
errorType: SchemaValidationErrorType,
requestBody: `{invalid json}`,
wantErr: true,
path: "test",
},
{
name: "Complex nested context",
errorType: SchemaValidationErrorType,
requestBody: `{"context": {"domain": "test-domain", "nested": {"key1": "value1", "key2": 123}}}`,
wantStatus: "NACK",
wantErrCode: "400",
wantErrMsg: "Schema validation failed",
wantErr: false,
path: "test",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := Nack(ctx, tt.errorType, tt.path, []byte(tt.requestBody))
if (err != nil) != tt.wantErr {
t.Errorf("Nack() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != nil {
return
}
var becknResp BecknResponse
if err := json.Unmarshal(resp, &becknResp); err != nil {
t.Errorf("Failed to unmarshal response: %v", err)
return
}
if becknResp.Message.Ack.Status != tt.wantStatus {
t.Errorf("Nack() status = %v, want %v", becknResp.Message.Ack.Status, tt.wantStatus)
}
if becknResp.Message.Error.Code != tt.wantErrCode {
t.Errorf("Nack() error code = %v, want %v", becknResp.Message.Error.Code, tt.wantErrCode)
}
if becknResp.Message.Error.Message != tt.wantErrMsg {
t.Errorf("Nack() error message = %v, want %v", becknResp.Message.Error.Message, tt.wantErrMsg)
}
var origReq BecknRequest
if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil {
if !compareContexts(becknResp.Context, origReq.Context) {
t.Errorf("Nack() context not preserved, got = %v, want %v", becknResp.Context, origReq.Context)
}
}
})
}
}
func TestAck(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
requestBody string
wantStatus string
wantErr bool
}{
{
name: "Valid request",
requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`,
wantStatus: "ACK",
wantErr: false,
},
{
name: "Empty context",
requestBody: `{"context": {}}`,
wantStatus: "ACK",
wantErr: false,
},
{
name: "Invalid JSON",
requestBody: `{invalid json}`,
wantErr: true,
},
{
name: "Complex nested context",
requestBody: `{"context": {"domain": "test-domain", "nested": {"key1": "value1", "key2": 123, "array": [1,2,3]}}}`,
wantStatus: "ACK",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := Ack(ctx, []byte(tt.requestBody))
if (err != nil) != tt.wantErr {
t.Errorf("Ack() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != nil {
return
}
var becknResp BecknResponse
if err := json.Unmarshal(resp, &becknResp); err != nil {
t.Errorf("Failed to unmarshal response: %v", err)
return
}
if becknResp.Message.Ack.Status != tt.wantStatus {
t.Errorf("Ack() status = %v, want %v", becknResp.Message.Ack.Status, tt.wantStatus)
}
if becknResp.Message.Error != nil {
t.Errorf("Ack() should not have error, got %v", becknResp.Message.Error)
}
var origReq BecknRequest
if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil {
if !compareContexts(becknResp.Context, origReq.Context) {
t.Errorf("Ack() context not preserved, got = %v, want %v", becknResp.Context, origReq.Context)
}
}
})
}
}
func TestHandleClientFailure(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
errorType ErrorType
requestBody string
wantErrCode string
wantErrMsg string
wantErr bool
}{
{
name: "Schema validation error",
errorType: SchemaValidationErrorType,
requestBody: `{"context": {"domain": "test-domain", "location": "test-location"}}`,
wantErrCode: "400",
wantErrMsg: "Schema validation failed",
wantErr: false,
},
{
name: "Invalid request error",
errorType: InvalidRequestErrorType,
requestBody: `{"context": {"domain": "test-domain"}}`,
wantErrCode: "401",
wantErrMsg: "Invalid request format",
wantErr: false,
},
{
name: "Unknown error type",
errorType: "UNKNOWN_ERROR",
requestBody: `{"context": {"domain": "test-domain"}}`,
wantErrCode: "500",
wantErrMsg: "Internal server error",
wantErr: false,
},
{
name: "Invalid JSON",
errorType: SchemaValidationErrorType,
requestBody: `{invalid json}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := HandleClientFailure(ctx, tt.errorType, []byte(tt.requestBody))
if (err != nil) != tt.wantErr {
t.Errorf("HandleClientFailure() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err != nil {
return
}
var failureResp ClientFailureBecknResponse
if err := json.Unmarshal(resp, &failureResp); err != nil {
t.Errorf("Failed to unmarshal response: %v", err)
return
}
if failureResp.Error.Code != tt.wantErrCode {
t.Errorf("HandleClientFailure() error code = %v, want %v", failureResp.Error.Code, tt.wantErrCode)
}
if failureResp.Error.Message != tt.wantErrMsg {
t.Errorf("HandleClientFailure() error message = %v, want %v", failureResp.Error.Message, tt.wantErrMsg)
}
var origReq BecknRequest
if err := json.Unmarshal([]byte(tt.requestBody), &origReq); err == nil {
if !compareContexts(failureResp.Context, origReq.Context) {
t.Errorf("HandleClientFailure() context not preserved, got = %v, want %v", failureResp.Context, origReq.Context)
}
}
})
}
}
func TestErrorMap(t *testing.T) {
expectedTypes := []ErrorType{
SchemaValidationErrorType,
InvalidRequestErrorType,
}
for _, tp := range expectedTypes {
if _, exists := errorMap[tp]; !exists {
t.Errorf("ErrorType %v not found in errorMap", tp)
}
}
if DefaultError.Code != "500" || DefaultError.Message != "Internal server error" {
t.Errorf("DefaultError not set correctly, got code=%v, message=%v", DefaultError.Code, DefaultError.Message)
}
}
func compareContexts(c1, c2 map[string]interface{}) bool {
if c1 == nil && c2 == nil {
return true
}
if c1 == nil && len(c2) == 0 || c2 == nil && len(c1) == 0 {
return true
}
return reflect.DeepEqual(c1, c2)
}