Changes to Response Error struct
This commit is contained in:
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @ashishkgGoogle @poojajoshi2 @Deepa-Mulchandani @tanyamadaan
|
||||||
88
.github/workflows/beckn_ci.yml
vendored
Normal file
88
.github/workflows/beckn_ci.yml
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- beckn-onix-v1.0-develop
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_DIRECTORY: "shared/plugin" # Root directory to start searching from
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint_and_test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
timeout-minutes: 10 # Increased timeout due to additional steps
|
||||||
|
steps:
|
||||||
|
# 1. Checkout the code from the test branch (triggered by PR)
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 2. Set up Go environment
|
||||||
|
- name: Set up Go 1.24.0
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.24.0'
|
||||||
|
|
||||||
|
# 3. Install golangci-lint
|
||||||
|
- name: Install golangci-lint
|
||||||
|
run: |
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
# 4. Run golangci-lint on the entire repo, starting from the root directory
|
||||||
|
- name: Run golangci-lint
|
||||||
|
run: |
|
||||||
|
golangci-lint run ./... # This will lint all Go files in the repo and subdirectories
|
||||||
|
|
||||||
|
# 5. Run unit tests with coverage in the entire repository
|
||||||
|
- name: Run unit tests with coverage
|
||||||
|
run: |
|
||||||
|
# Create a directory to store coverage files
|
||||||
|
mkdir -p $GITHUB_WORKSPACE/coverage_files
|
||||||
|
|
||||||
|
# Find all *_test.go files and run `go test` for each
|
||||||
|
find ./ -type f -name '*_test.go' | while read test_file; do
|
||||||
|
# Get the directory of the test file
|
||||||
|
test_dir=$(dirname "$test_file")
|
||||||
|
# Get the name of the Go file associated with the test
|
||||||
|
go_file="${test_file/_test.go/.go}"
|
||||||
|
|
||||||
|
# Run tests and store coverage for each Go file in a separate file
|
||||||
|
echo "Running tests in $test_dir for $go_file"
|
||||||
|
go test -v -coverprofile=$GITHUB_WORKSPACE/coverage_files/coverage_$(basename "$go_file" .go).out $test_dir
|
||||||
|
done
|
||||||
|
|
||||||
|
# 6. List the generated coverage files for debugging purposes
|
||||||
|
#- name: List coverage files
|
||||||
|
#run: |
|
||||||
|
#echo "Listing all generated coverage files:"
|
||||||
|
#ls -l $GITHUB_WORKSPACE/coverage_files/
|
||||||
|
|
||||||
|
# 7. Check coverage for each generated coverage file
|
||||||
|
- name: Check coverage for each test file
|
||||||
|
run: |
|
||||||
|
# Loop through each coverage file in the coverage_files directory
|
||||||
|
for coverage_file in $GITHUB_WORKSPACE/coverage_files/coverage_*.out; do
|
||||||
|
echo "Checking coverage for $coverage_file"
|
||||||
|
|
||||||
|
# Get the coverage percentage for each file
|
||||||
|
coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//')
|
||||||
|
echo "Coverage for $coverage_file: $coverage%"
|
||||||
|
|
||||||
|
# If coverage is below threshold (80%), fail the job
|
||||||
|
if (( $(echo "$coverage < 80" | bc -l) )); then
|
||||||
|
echo "Coverage for $coverage_file is below 80%. Failing the job."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 7. Build the Go code
|
||||||
|
#- name: Build Go code
|
||||||
|
# run: |
|
||||||
|
# go build -o myapp ${{ env.APP_DIRECTORY }}/...
|
||||||
|
# if [ ! -f myapp ]; then
|
||||||
|
# echo "Build failed: myapp executable was not created."
|
||||||
|
# exit 1
|
||||||
|
# else
|
||||||
|
# echo "Build succeeded: myapp executable created."
|
||||||
|
# fi
|
||||||
36
.github/workflows/ci.yml
vendored
Normal file
36
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Go CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- beck-onix-v1.0-develop
|
||||||
|
- beck-onix-v1.0
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- beck-onix-v1.0-develop
|
||||||
|
- beck-onix-v1.0
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '1.20'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: go mod tidy
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -coverprofile=coverage.out ./...
|
||||||
|
- name: Check coverage
|
||||||
|
run: |
|
||||||
|
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||||
|
if (( $(echo "$coverage < 90" | bc -l) )); then
|
||||||
|
echo "Coverage is below 90%"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Run golangci-lint
|
||||||
|
run: golangci-lint run
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
files: ./coverage.out
|
||||||
8
go.mod
8
go.mod
@@ -1,10 +1,16 @@
|
|||||||
module github.com/beckn/beckn-onix
|
module github.com/beckn/beckn-onix
|
||||||
|
|
||||||
go 1.23.4
|
go 1.23.4
|
||||||
|
toolchain go1.23.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.14.0 // indirect
|
require (
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
8
go.sum
8
go.sum
@@ -2,8 +2,12 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK
|
|||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
|
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{}
|
type schemaValidatorProvider struct{}
|
||||||
|
|
||||||
// New initializes a new Verifier instance.
|
// 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'")
|
return nil, nil, errors.New("config must contain 'schema_dir'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new Validator instance with the provided configuration
|
// Create a new schemaValidator instance with the provided configuration
|
||||||
return validator.New(ctx, &validator.Config{
|
return schemaValidator.New(ctx, &schemaValidator.Config{
|
||||||
SchemaDir: schemaDir, // Pass the schemaDir to the validator.Config
|
SchemaDir: schemaDir,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider is the exported symbol that the plugin manager will look for.
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
var Provider definition.SchemaValidatorProvider = &schemaValidatorProvider{}
|
var Provider definition.SchemaValidatorProvider = schemaValidatorProvider{}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func TestValidatorProviderSuccess(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) {
|
||||||
vp := schemaValidatorProvider{}
|
vp := schemaValidatorProvider{}
|
||||||
validator, close, err := vp.New(tt.ctx, tt.config)
|
schemaValidator, close, err := vp.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
// Ensure no error occurred
|
// Ensure no error occurred
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,9 +80,9 @@ func TestValidatorProviderSuccess(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the validator is not nil
|
// Ensure the schemaValidator is not nil
|
||||||
if validator == nil {
|
if schemaValidator == nil {
|
||||||
t.Error("expected a non-nil validator, got nil")
|
t.Error("expected a non-nil schemaValidator, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the close function is not nil
|
// Ensure the close function is not nil
|
||||||
@@ -110,11 +110,23 @@ func TestValidatorProviderFailure(t *testing.T) {
|
|||||||
config map[string]string
|
config map[string]string
|
||||||
expectedError 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",
|
name: "Invalid schema directory",
|
||||||
ctx: context.Background(), // Valid context
|
ctx: context.Background(), // Valid context
|
||||||
config: map[string]string{"schema_dir": "/invalid/dir"},
|
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",
|
name: "Nil context",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
definition "github.com/beckn/beckn-onix/pkg/plugin/definition"
|
response "github.com/beckn/beckn-onix/pkg/response"
|
||||||
|
|
||||||
"github.com/santhosh-tekuri/jsonschema/v6"
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||||
)
|
)
|
||||||
@@ -23,12 +23,13 @@ type payload struct {
|
|||||||
} `json:"context"`
|
} `json:"context"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator implements the Validator interface.
|
// SchemaValidator implements the Validator interface.
|
||||||
type SchemaValidator struct {
|
type SchemaValidator struct {
|
||||||
config *Config
|
config *Config
|
||||||
schemaCache map[string]*jsonschema.Schema
|
schemaCache map[string]*jsonschema.Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config struct for SchemaValidator.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
SchemaDir string
|
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
|
// Call Initialise function to load schemas and get validators
|
||||||
if err := v.Initialise(); err != nil {
|
if err := v.initialise(); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to initialise validator: %v", err)
|
return nil, nil, fmt.Errorf("failed to initialise schemaValidator: %v", err)
|
||||||
}
|
}
|
||||||
return v, v.Close, nil
|
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
|
// Handle schema validation errors
|
||||||
if validationErr, ok := err.(*jsonschema.ValidationError); ok {
|
if validationErr, ok := err.(*jsonschema.ValidationError); ok {
|
||||||
// Convert validation errors into an array of SchemaValError
|
// Convert validation errors into an array of SchemaValError
|
||||||
var schemaErrors []definition.SchemaValError
|
var schemaErrors []response.Error
|
||||||
for _, cause := range validationErr.Causes {
|
for _, cause := range validationErr.Causes {
|
||||||
// Extract the path and message from the validation error
|
// Extract the path and message from the validation error
|
||||||
path := strings.Join(cause.InstanceLocation, ".") // JSON path to the invalid field
|
path := strings.Join(cause.InstanceLocation, ".") // JSON path to the invalid field
|
||||||
message := cause.Error() // Validation error message
|
message := cause.Error() // Validation error message
|
||||||
|
|
||||||
// Append the error to the schemaErrors array
|
// Append the error to the schemaErrors array
|
||||||
schemaErrors = append(schemaErrors, definition.SchemaValError{
|
schemaErrors = append(schemaErrors, response.Error{
|
||||||
Path: path,
|
Paths: path,
|
||||||
Message: message,
|
Message: message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Return the array of schema validation errors
|
// 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 a generic error for non-validation errors
|
||||||
return fmt.Errorf("validation failed: %v", err)
|
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
|
// 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.
|
// 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
|
schemaDir := v.config.SchemaDir
|
||||||
// Check if the directory exists and is accessible.
|
// Check if the directory exists and is accessible.
|
||||||
info, err := os.Stat(schemaDir)
|
info, err := os.Stat(schemaDir)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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.
|
// 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) {
|
func TestValidator_Validate_Success(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
payload string
|
payload string
|
||||||
wantValid bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid payload",
|
name: "Valid payload",
|
||||||
url: "http://example.com/endpoint",
|
url: "http://example.com/endpoint",
|
||||||
payload: `{"context": {"domain": "example", "version": "1.0", "action": "endpoint"}}`,
|
payload: `{"context": {"domain": "example", "version": "1.0", "action": "endpoint"}}`,
|
||||||
wantValid: true,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,12 +80,11 @@ func TestValidator_Validate_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) {
|
||||||
u, _ := url.Parse(tt.url)
|
u, _ := url.Parse(tt.url)
|
||||||
valid, err := v.Validate(context.Background(), u, []byte(tt.payload))
|
err := v.Validate(context.Background(), u, []byte(tt.payload))
|
||||||
if err != (definition.SchemaValError{}) {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error: %v", err)
|
t.Errorf("Unexpected error: %v", err)
|
||||||
}
|
} else {
|
||||||
if valid != tt.wantValid {
|
t.Logf("Test %s passed with no errors", tt.name)
|
||||||
t.Errorf("Error: Validate() returned valid = %v, expected valid = %v", valid, tt.wantValid)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -93,32 +92,28 @@ func TestValidator_Validate_Success(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidator_Validate_Failure(t *testing.T) {
|
func TestValidator_Validate_Failure(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
url string
|
url string
|
||||||
payload string
|
payload string
|
||||||
wantValid bool
|
wantErr string
|
||||||
wantErr string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Invalid JSON payload",
|
name: "Invalid JSON payload",
|
||||||
url: "http://example.com/endpoint",
|
url: "http://example.com/endpoint",
|
||||||
payload: `{"context": {"domain": "example", "version": "1.0"`,
|
payload: `{"context": {"domain": "example", "version": "1.0"`,
|
||||||
wantValid: false,
|
wantErr: "failed to parse JSON payload",
|
||||||
wantErr: "failed to parse JSON payload",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Schema validation failure",
|
name: "Schema validation failure",
|
||||||
url: "http://example.com/endpoint",
|
url: "http://example.com/endpoint",
|
||||||
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
|
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
|
||||||
wantValid: false,
|
wantErr: "context: at '/context': missing property 'action'",
|
||||||
wantErr: "Validation failed",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Schema not found",
|
name: "Schema not found",
|
||||||
url: "http://example.com/unknown_endpoint",
|
url: "http://example.com/unknown_endpoint",
|
||||||
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
|
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
|
||||||
wantValid: false,
|
wantErr: "schema not found for domain",
|
||||||
wantErr: "schema not found for domain",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,13 +130,21 @@ func TestValidator_Validate_Failure(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) {
|
||||||
u, _ := url.Parse(tt.url)
|
u, _ := url.Parse(tt.url)
|
||||||
valid, err := v.Validate(context.Background(), u, []byte(tt.payload))
|
err := v.Validate(context.Background(), u, []byte(tt.payload))
|
||||||
if (err != (definition.SchemaValError{}) && !strings.Contains(err.Message, tt.wantErr)) || (err == (definition.SchemaValError{}) && tt.wantErr != "") {
|
if tt.wantErr != "" {
|
||||||
t.Errorf("Error: Validate() returned error = %v, expected error = %v", err, tt.wantErr)
|
if err == nil {
|
||||||
return
|
t.Errorf("Expected error containing '%s', but got nil", tt.wantErr)
|
||||||
}
|
} else if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
if valid != tt.wantValid {
|
t.Errorf("Expected error containing '%s', but got '%v'", tt.wantErr, err)
|
||||||
t.Errorf("Validate() returned valid = %v, expected valid = %v", valid, tt.wantValid)
|
} 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}
|
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 != "") {
|
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 {
|
} else if err == nil {
|
||||||
t.Logf("Test %s passed: validator initialized successfully", tt.name)
|
t.Logf("Test %s passed: validator initialized successfully", tt.name)
|
||||||
} else {
|
} else {
|
||||||
@@ -309,22 +315,22 @@ func TestValidator_New_Failure(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: "config cannot be nil",
|
wantErr: "config cannot be nil",
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: "Config is empty",
|
// name: "Config is empty",
|
||||||
config: &Config{},
|
// config: &Config{},
|
||||||
setupFunc: func(schemaDir string) error {
|
// setupFunc: func(schemaDir string) error {
|
||||||
return nil
|
// return nil
|
||||||
},
|
// },
|
||||||
wantErr: "config must contain 'schema_dir'",
|
// wantErr: "config must contain 'schema_dir'",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "schema_dir is empty",
|
// name: "schema_dir is empty",
|
||||||
config: &Config{SchemaDir: ""},
|
// config: &Config{SchemaDir: ""},
|
||||||
setupFunc: func(schemaDir string) error {
|
// setupFunc: func(schemaDir string) error {
|
||||||
return nil
|
// return nil
|
||||||
},
|
// },
|
||||||
wantErr: "config must contain 'schema_dir'",
|
// wantErr: "config must contain 'schema_dir'",
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: "Failed to initialise validators",
|
name: "Failed to initialise validators",
|
||||||
config: &Config{
|
config: &Config{
|
||||||
@@ -334,7 +340,7 @@ func TestValidator_New_Failure(t *testing.T) {
|
|||||||
// Do not create the schema directory
|
// Do not create the schema directory
|
||||||
return nil
|
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
143
pkg/response/response.go
Normal 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)
|
||||||
|
}
|
||||||
303
pkg/response/response_test.go
Normal file
303
pkg/response/response_test.go
Normal 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)
|
||||||
|
}
|
||||||
22
shared/plugin/definition/signVerifier.go
Normal file
22
shared/plugin/definition/signVerifier.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Verifier defines the method for verifying signatures.
|
||||||
|
type Verifier interface {
|
||||||
|
// Verify checks the validity of the signature for the given body.
|
||||||
|
Verify(ctx context.Context, body []byte, header []byte, publicKeyBase64 string) (bool, error)
|
||||||
|
Close() error // Close for releasing resources
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifierProvider initializes a new Verifier instance with the given config.
|
||||||
|
type VerifierProvider interface {
|
||||||
|
// New creates a new Verifier instance based on the provided config.
|
||||||
|
New(ctx context.Context, config map[string]string) (Verifier, func() error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyManager is the interface for key management plugin.
|
||||||
|
type PublicKeyManager interface {
|
||||||
|
// PublicKey retrieves the public key for the given subscriberID and keyID.
|
||||||
|
PublicKey(ctx context.Context, subscriberID string, keyID string) (string, error)
|
||||||
|
}
|
||||||
24
shared/plugin/definition/signer.go
Normal file
24
shared/plugin/definition/signer.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Signer defines the method for signing.
|
||||||
|
type Signer interface {
|
||||||
|
// Sign generates a signature for the given body and privateKeyBase64.
|
||||||
|
// The signature is created with the given timestamps: createdAt (signature creation time)
|
||||||
|
// and expiresAt (signature expiration time).
|
||||||
|
Sign(ctx context.Context, body []byte, privateKeyBase64 string, createdAt, expiresAt int64) (string, error)
|
||||||
|
Close() error // Close for releasing resources
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerProvider initializes a new signer instance with the given config.
|
||||||
|
type SignerProvider interface {
|
||||||
|
// New creates a new signer instance based on the provided config.
|
||||||
|
New(ctx context.Context, config map[string]string) (Signer, func() error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyManager is the interface for key management plugin.
|
||||||
|
type PrivateKeyManager interface {
|
||||||
|
// PrivateKey retrieves the private key for the given subscriberID and keyID.
|
||||||
|
PrivateKey(ctx context.Context, subscriberID string, keyID string) (string, error)
|
||||||
|
}
|
||||||
26
shared/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
26
shared/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/shared/plugin/definition"
|
||||||
|
|
||||||
|
plugin "github.com/beckn/beckn-onix/shared/plugin/definition"
|
||||||
|
verifier "github.com/beckn/beckn-onix/shared/plugin/implementation/signVerifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifierProvider provides instances of Verifier.
|
||||||
|
type VerifierProvider struct{}
|
||||||
|
|
||||||
|
// New initializes a new Verifier instance.
|
||||||
|
func (vp VerifierProvider) New(ctx context.Context, config map[string]string) (plugin.Verifier, func() error, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, errors.New("context cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifier.New(ctx, &verifier.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider definition.VerifierProvider = VerifierProvider{}
|
||||||
89
shared/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
89
shared/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestVerifierProviderSuccess tests successful creation of a verifier.
|
||||||
|
func TestVerifierProviderSuccess(t *testing.T) {
|
||||||
|
provider := VerifierProvider{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful creation",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil context",
|
||||||
|
ctx: context.TODO(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
verifier, close, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
if verifier == nil {
|
||||||
|
t.Fatal("Expected verifier instance to be non-nil")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVerifierProviderFailure tests cases where verifier creation should fail.
|
||||||
|
func TestVerifierProviderFailure(t *testing.T) {
|
||||||
|
provider := VerifierProvider{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Nil context failure",
|
||||||
|
ctx: nil,
|
||||||
|
config: map[string]string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
verifierInstance, close, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("Expected error: %v, but got: %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
if verifierInstance != nil {
|
||||||
|
t.Fatal("Expected verifier instance to be nil")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
120
shared/plugin/implementation/signVerifier/signVerifier.go
Normal file
120
shared/plugin/implementation/signVerifier/signVerifier.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package verifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config struct for Verifier.
|
||||||
|
type Config struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifier implements the Verifier interface.
|
||||||
|
type Verifier struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Verifier instance.
|
||||||
|
func New(ctx context.Context, config *Config) (*Verifier, func() error, error) {
|
||||||
|
v := &Verifier{config: config}
|
||||||
|
|
||||||
|
return v, v.Close, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks the signature for the given payload and public key.
|
||||||
|
func (v *Verifier) Verify(ctx context.Context, body []byte, header []byte, publicKeyBase64 string) (bool, error) {
|
||||||
|
createdTimestamp, expiredTimestamp, signature, err := parseAuthHeader(string(header))
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return false, fmt.Errorf("error parsing header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return false, fmt.Errorf("error decoding signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
if createdTimestamp > currentTime || currentTime > expiredTimestamp {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return false, fmt.Errorf("signature is expired or not yet valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
createdTime := time.Unix(createdTimestamp, 0)
|
||||||
|
expiredTime := time.Unix(expiredTimestamp, 0)
|
||||||
|
|
||||||
|
signingString := hash(body, createdTime.Unix(), expiredTime.Unix())
|
||||||
|
|
||||||
|
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return false, fmt.Errorf("error decoding public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ed25519.Verify(ed25519.PublicKey(decodedPublicKey), []byte(signingString), signatureBytes) {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return false, fmt.Errorf("signature verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAuthHeader extracts signature values from the Authorization header.
|
||||||
|
func parseAuthHeader(header string) (int64, int64, string, error) {
|
||||||
|
header = strings.TrimPrefix(header, "Signature ")
|
||||||
|
|
||||||
|
parts := strings.Split(header, ",")
|
||||||
|
signatureMap := make(map[string]string)
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
||||||
|
if len(keyValue) == 2 {
|
||||||
|
key := strings.TrimSpace(keyValue[0])
|
||||||
|
value := strings.Trim(keyValue[1], "\"")
|
||||||
|
signatureMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createdTimestamp, err := strconv.ParseInt(signatureMap["created"], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return 0, 0, "", fmt.Errorf("invalid created timestamp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiredTimestamp, err := strconv.ParseInt(signatureMap["expires"], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return 0, 0, "", fmt.Errorf("invalid expires timestamp: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := signatureMap["signature"]
|
||||||
|
if signature == "" {
|
||||||
|
// TODO: Return appropriate error code when Error Code Handling Module is ready
|
||||||
|
return 0, 0, "", fmt.Errorf("signature missing in header")
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdTimestamp, expiredTimestamp, signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash constructs a signing string for verification.
|
||||||
|
func hash(payload []byte, createdTimestamp, expiredTimestamp int64) string {
|
||||||
|
hasher, _ := blake2b.New512(nil)
|
||||||
|
hasher.Write(payload)
|
||||||
|
hashSum := hasher.Sum(nil)
|
||||||
|
digestB64 := base64.StdEncoding.EncodeToString(hashSum)
|
||||||
|
|
||||||
|
return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdTimestamp, expiredTimestamp, digestB64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases resources (mock implementation returning nil).
|
||||||
|
func (v *Verifier) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
153
shared/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
153
shared/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package verifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateTestKeyPair generates a new ED25519 key pair for testing.
|
||||||
|
func generateTestKeyPair() (string, string) {
|
||||||
|
publicKey, privateKey, _ := ed25519.GenerateKey(nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// signTestData creates a valid signature for test cases.
|
||||||
|
func signTestData(privateKeyBase64 string, body []byte, createdAt, expiresAt int64) string {
|
||||||
|
privateKeyBytes, _ := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||||
|
privateKey := ed25519.PrivateKey(privateKeyBytes)
|
||||||
|
|
||||||
|
signingString := hash(body, createdAt, expiresAt)
|
||||||
|
signature := ed25519.Sign(privateKey, []byte(signingString))
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVerifySuccessCases tests all valid signature verification cases.
|
||||||
|
func TestVerifySuccess(t *testing.T) {
|
||||||
|
privateKeyBase64, publicKeyBase64 := generateTestKeyPair()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
body []byte
|
||||||
|
createdAt int64
|
||||||
|
expiresAt int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Signature",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
createdAt: time.Now().Unix(),
|
||||||
|
expiresAt: time.Now().Unix() + 3600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
signature := signTestData(privateKeyBase64, tt.body, tt.createdAt, tt.expiresAt)
|
||||||
|
header := `Signature created="` + strconv.FormatInt(tt.createdAt, 10) +
|
||||||
|
`", expires="` + strconv.FormatInt(tt.expiresAt, 10) +
|
||||||
|
`", signature="` + signature + `"`
|
||||||
|
|
||||||
|
verifier, close, _ := New(context.Background(), &Config{})
|
||||||
|
valid, err := verifier.Verify(context.Background(), tt.body, []byte(header), publicKeyBase64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
t.Fatal("Expected signature verification to succeed")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVerifyFailureCases tests all invalid signature verification cases.
|
||||||
|
func TestVerifyFailure(t *testing.T) {
|
||||||
|
privateKeyBase64, publicKeyBase64 := generateTestKeyPair()
|
||||||
|
_, wrongPublicKeyBase64 := generateTestKeyPair()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
body []byte
|
||||||
|
header string
|
||||||
|
pubKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Missing Authorization Header",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: "",
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Malformed Header",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `InvalidSignature created="wrong"`,
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Base64 Signature",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||||
|
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) +
|
||||||
|
`", signature="!!INVALIDBASE64!!"`,
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expired Signature",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `Signature created="` + strconv.FormatInt(time.Now().Unix()-7200, 10) +
|
||||||
|
`", expires="` + strconv.FormatInt(time.Now().Unix()-3600, 10) +
|
||||||
|
`", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix()-7200, time.Now().Unix()-3600) + `"`,
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Public Key",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||||
|
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) +
|
||||||
|
`", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix(), time.Now().Unix()+3600) + `"`,
|
||||||
|
pubKey: wrongPublicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid Expires Timestamp",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||||
|
`", expires="invalid_timestamp"`,
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Signature Missing in Headers",
|
||||||
|
body: []byte("Test Payload"),
|
||||||
|
header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) +
|
||||||
|
`", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) + `"`,
|
||||||
|
pubKey: publicKeyBase64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
verifier, close, _ := New(context.Background(), &Config{})
|
||||||
|
valid, err := verifier.Verify(context.Background(), tt.body, []byte(tt.header), tt.pubKey)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error but got none")
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
t.Fatal("Expected verification to fail")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
shared/plugin/implementation/signer/cmd/plugin.go
Normal file
24
shared/plugin/implementation/signer/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/shared/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/shared/plugin/implementation/signer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignerProvider implements the definition.SignerProvider interface.
|
||||||
|
type SignerProvider struct{}
|
||||||
|
|
||||||
|
// New creates a new Signer instance using the provided configuration.
|
||||||
|
func (p SignerProvider) New(ctx context.Context, config map[string]string) (definition.Signer, func() error, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, errors.New("context cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return signer.New(ctx, &signer.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider definition.SignerProvider = SignerProvider{}
|
||||||
101
shared/plugin/implementation/signer/cmd/plugin_test.go
Normal file
101
shared/plugin/implementation/signer/cmd/plugin_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSignerProviderSuccess verifies successful scenarios for SignerProvider.
|
||||||
|
func TestSignerProviderSuccess(t *testing.T) {
|
||||||
|
provider := SignerProvider{}
|
||||||
|
|
||||||
|
successTests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unexpected Config Key",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"unexpected_key": "some_value"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config with empty TTL",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"ttl": ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config with negative TTL",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"ttl": "-100"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Config with non-numeric TTL",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"ttl": "not_a_number"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range successTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
signer, close, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: expected no error, but got: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
if signer == nil {
|
||||||
|
t.Fatalf("Test %q failed: signer instance should not be nil", tt.name)
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignerProviderFailure verifies failure scenarios for SignerProvider.
|
||||||
|
func TestSignerProviderFailure(t *testing.T) {
|
||||||
|
provider := SignerProvider{}
|
||||||
|
|
||||||
|
failureTests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Nil Context",
|
||||||
|
ctx: nil,
|
||||||
|
config: map[string]string{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range failureTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
signerInstance, close, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("Test %q failed: expected error: %v, got: %v", tt.name, tt.wantErr, err)
|
||||||
|
}
|
||||||
|
if signerInstance != nil {
|
||||||
|
t.Fatalf("Test %q failed: expected signer instance to be nil", tt.name)
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
t.Fatalf("Test %q failed: expected cleanup function to be nil", tt.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
77
shared/plugin/implementation/signer/signer.go
Normal file
77
shared/plugin/implementation/signer/signer.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package signer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds the configuration for the signing process.
|
||||||
|
type Config struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer implements the Signer interface and handles the signing process.
|
||||||
|
type Signer struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Signer instance with the given configuration.
|
||||||
|
func New(ctx context.Context, config *Config) (*Signer, func() error, error) {
|
||||||
|
s := &Signer{config: config}
|
||||||
|
|
||||||
|
return s, s.Close, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash generates a signing string using BLAKE-512 hashing.
|
||||||
|
func hash(payload []byte, createdAt, expiresAt int64) (string, error) {
|
||||||
|
hasher, _ := blake2b.New512(nil)
|
||||||
|
|
||||||
|
_, err := hasher.Write(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to hash payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashSum := hasher.Sum(nil)
|
||||||
|
digestB64 := base64.StdEncoding.EncodeToString(hashSum)
|
||||||
|
|
||||||
|
return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdAt, expiresAt, digestB64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSignature signs the given signing string using the provided private key.
|
||||||
|
func generateSignature(signingString []byte, privateKeyBase64 string) ([]byte, error) {
|
||||||
|
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(privateKeyBytes) != ed25519.PrivateKeySize {
|
||||||
|
return nil, errors.New("invalid private key length")
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey := ed25519.PrivateKey(privateKeyBytes)
|
||||||
|
return ed25519.Sign(privateKey, signingString), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign generates a digital signature for the provided payload.
|
||||||
|
func (s *Signer) Sign(ctx context.Context, body []byte, privateKeyBase64 string, createdAt, expiresAt int64) (string, error) {
|
||||||
|
signingString, err := hash(body, createdAt, expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := generateSignature([]byte(signingString), privateKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases resources (mock implementation returning nil).
|
||||||
|
func (s *Signer) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
104
shared/plugin/implementation/signer/signer_test.go
Normal file
104
shared/plugin/implementation/signer/signer_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package signer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateTestKeys generates a test private and public key pair in base64 encoding.
|
||||||
|
func generateTestKeys() (string, string) {
|
||||||
|
publicKey, privateKey, _ := ed25519.GenerateKey(nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignSuccess tests the Sign method with valid inputs to ensure it produces a valid signature.
|
||||||
|
func TestSignSuccess(t *testing.T) {
|
||||||
|
privateKey, _ := generateTestKeys()
|
||||||
|
config := Config{}
|
||||||
|
signer, close, _ := New(context.Background(), &config)
|
||||||
|
|
||||||
|
successTests := []struct {
|
||||||
|
name string
|
||||||
|
payload []byte
|
||||||
|
privateKey string
|
||||||
|
createdAt int64
|
||||||
|
expiresAt int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Signing",
|
||||||
|
payload: []byte("test payload"),
|
||||||
|
privateKey: privateKey,
|
||||||
|
createdAt: time.Now().Unix(),
|
||||||
|
expiresAt: time.Now().Unix() + 3600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range successTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
signature, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(signature) == 0 {
|
||||||
|
t.Errorf("expected a non-empty signature, but got empty")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignFailure tests the Sign method with invalid inputs to ensure proper error handling.
|
||||||
|
func TestSignFailure(t *testing.T) {
|
||||||
|
config := Config{}
|
||||||
|
signer, close, _ := New(context.Background(), &config)
|
||||||
|
|
||||||
|
failureTests := []struct {
|
||||||
|
name string
|
||||||
|
payload []byte
|
||||||
|
privateKey string
|
||||||
|
createdAt int64
|
||||||
|
expiresAt int64
|
||||||
|
expectErrString string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid Private Key",
|
||||||
|
payload: []byte("test payload"),
|
||||||
|
privateKey: "invalid_key",
|
||||||
|
createdAt: time.Now().Unix(),
|
||||||
|
expiresAt: time.Now().Unix() + 3600,
|
||||||
|
expectErrString: "error decoding private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Short Private Key",
|
||||||
|
payload: []byte("test payload"),
|
||||||
|
privateKey: base64.StdEncoding.EncodeToString([]byte("short_key")),
|
||||||
|
createdAt: time.Now().Unix(),
|
||||||
|
expiresAt: time.Now().Unix() + 3600,
|
||||||
|
expectErrString: "invalid private key length",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range failureTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got none")
|
||||||
|
} else if !strings.Contains(err.Error(), tt.expectErrString) {
|
||||||
|
t.Errorf("expected error message to contain %q, got %v", tt.expectErrString, err)
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Cleanup function returned an error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
108
shared/plugin/manager.go
Normal file
108
shared/plugin/manager.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/shared/plugin/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the plugin manager configuration.
|
||||||
|
type Config struct {
|
||||||
|
Root string `yaml:"root"`
|
||||||
|
Signer PluginConfig `yaml:"signer"`
|
||||||
|
Verifier PluginConfig `yaml:"verifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginConfig represents configuration details for a plugin.
|
||||||
|
type PluginConfig struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Config map[string]string `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager handles dynamic plugin loading and management.
|
||||||
|
type Manager struct {
|
||||||
|
sp definition.SignerProvider
|
||||||
|
vp definition.VerifierProvider
|
||||||
|
cfg *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager initializes a new Manager with the given configuration file.
|
||||||
|
func NewManager(ctx context.Context, cfg *Config) (*Manager, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("configuration cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load signer plugin
|
||||||
|
sp, err := provider[definition.SignerProvider](cfg.Root, cfg.Signer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load signer plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load verifier plugin
|
||||||
|
vp, err := provider[definition.VerifierProvider](cfg.Root, cfg.Verifier.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load Verifier plugin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{sp: sp, vp: vp, cfg: cfg}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// provider loads a plugin dynamically and retrieves its provider instance.
|
||||||
|
func provider[T any](root, id string) (T, error) {
|
||||||
|
var zero T
|
||||||
|
if len(strings.TrimSpace(id)) == 0 {
|
||||||
|
return zero, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := plugin.Open(pluginPath(root, id))
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("failed to open plugin %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
symbol, err := p.Lookup("Provider")
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("failed to find Provider symbol in plugin %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prov, ok := symbol.(*T)
|
||||||
|
if !ok {
|
||||||
|
return zero, fmt.Errorf("failed to cast Provider for %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *prov, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginPath constructs the path to the plugin shared object file.
|
||||||
|
func pluginPath(root, id string) string {
|
||||||
|
return filepath.Join(root, id+".so")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer retrieves the signing plugin instance.
|
||||||
|
func (m *Manager) Signer(ctx context.Context) (definition.Signer, func() error, error) {
|
||||||
|
if m.sp == nil {
|
||||||
|
return nil, nil, fmt.Errorf("signing plugin provider not loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, close, err := m.sp.New(ctx, m.cfg.Signer.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to initialize signer: %w", err)
|
||||||
|
}
|
||||||
|
return signer, close, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifier retrieves the verification plugin instance.
|
||||||
|
func (m *Manager) Verifier(ctx context.Context) (definition.Verifier, func() error, error) {
|
||||||
|
if m.vp == nil {
|
||||||
|
return nil, nil, fmt.Errorf("Verifier plugin provider not loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
Verifier, close, err := m.vp.New(ctx, m.cfg.Verifier.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to initialize Verifier: %w", err)
|
||||||
|
}
|
||||||
|
return Verifier, close, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user