solve schema validation conflicts
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 (90%), fail the job
|
||||||
|
if (( $(echo "$coverage < 80" | bc -l) )); then
|
||||||
|
echo "Coverage for $coverage_file is below 90%. 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
|
||||||
85
.github/workflows/beckn_ci_test.yml
vendored
Normal file
85
.github/workflows/beckn_ci_test.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: CI/CD Test Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- beckn-onix-v1.0-develop
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_DIRECTORY: "shared/plugin"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint_and_test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
timeout-minutes: 10
|
||||||
|
outputs:
|
||||||
|
coverage_ok: ${{ steps.coverage_check.outputs.coverage_ok }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go 1.24.0
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.24.0'
|
||||||
|
|
||||||
|
- name: Install golangci-lint
|
||||||
|
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
run: golangci-lint run ./...
|
||||||
|
|
||||||
|
- name: Run unit tests with coverage
|
||||||
|
run: |
|
||||||
|
mkdir -p $GITHUB_WORKSPACE/coverage_files
|
||||||
|
test_files=$(find ./ -type f -name '*_test.go')
|
||||||
|
if [ -z "$test_files" ]; then
|
||||||
|
echo "No test cases found. Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for test_file in $test_files; do
|
||||||
|
test_dir=$(dirname "$test_file")
|
||||||
|
go_file="${test_file/_test.go/.go}"
|
||||||
|
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 || echo "Tests failed, but continuing."
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check coverage for each test file
|
||||||
|
id: coverage_check
|
||||||
|
run: |
|
||||||
|
echo "coverage_ok=true" >> $GITHUB_OUTPUT
|
||||||
|
coverage_files=$(find $GITHUB_WORKSPACE/coverage_files -name "coverage_*.out")
|
||||||
|
if [ -z "$coverage_files" ]; then
|
||||||
|
echo "No coverage files found. Skipping coverage check."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for coverage_file in $coverage_files; do
|
||||||
|
echo "Checking coverage for $coverage_file"
|
||||||
|
coverage=$(go tool cover -func=$coverage_file | grep total | awk '{print $3}' | sed 's/%//')
|
||||||
|
echo "Coverage: $coverage%"
|
||||||
|
if (( $(echo "$coverage < 90" | bc -l) )); then
|
||||||
|
echo "coverage_ok=false" >> $GITHUB_OUTPUT
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
require_exception_approval:
|
||||||
|
needs: lint_and_test
|
||||||
|
if: needs.lint_and_test.outputs.coverage_ok == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: coverage-exception
|
||||||
|
url: https://your-coverage-dashboard.com # Optional
|
||||||
|
steps:
|
||||||
|
- name: Manual approval required
|
||||||
|
run: echo "Coverage < 90%. Approval required to continue."
|
||||||
|
|
||||||
|
proceed_with_merge:
|
||||||
|
needs: [lint_and_test, require_exception_approval]
|
||||||
|
if: |
|
||||||
|
needs.lint_and_test.outputs.coverage_ok == 'true' || success()
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Proceed with merge
|
||||||
|
run: echo "Coverage requirement met or exception approved. Merge allowed."
|
||||||
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
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -129,3 +129,13 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# Ignore compiled shared object files
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Ignore coverage output files
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# Ignore the schema directory used for testing
|
||||||
|
/schemas/
|
||||||
25
Dockerfile.adapter
Normal file
25
Dockerfile.adapter
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM golang:1.24-bullseye AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY cmd/adapter ./cmd/adapter
|
||||||
|
COPY core/ ./core
|
||||||
|
COPY pkg/ ./pkg
|
||||||
|
COPY go.mod .
|
||||||
|
COPY go.sum .
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
RUN go build -o server cmd/adapter/main.go
|
||||||
|
|
||||||
|
# Create a minimal runtime image
|
||||||
|
FROM cgr.dev/chainguard/wolfi-base
|
||||||
|
# ✅ Alpine is removed; using minimal Debian
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the built binary and plugin
|
||||||
|
COPY --from=builder /app/server .
|
||||||
|
|
||||||
|
# Expose port 8080
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the Go server with the config flag from environment variable.
|
||||||
|
CMD ["sh", "-c", "./server --config=${CONFIG_FILE}"]
|
||||||
181
cmd/adapter/main.go
Normal file
181
cmd/adapter/main.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/core/module"
|
||||||
|
"github.com/beckn/beckn-onix/core/module/handler"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config struct holds all configurations.
|
||||||
|
type Config struct {
|
||||||
|
AppName string `yaml:"appName"`
|
||||||
|
Log log.Config `yaml:"log"`
|
||||||
|
PluginManager *plugin.ManagerConfig `yaml:"pluginManager"`
|
||||||
|
Modules []module.Config `yaml:"modules"`
|
||||||
|
HTTP httpConfig `yaml:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpConfig struct {
|
||||||
|
Port string `yaml:"port"`
|
||||||
|
Timeouts timeoutConfig `yaml:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type timeoutConfig struct {
|
||||||
|
Read time.Duration `yaml:"read"`
|
||||||
|
Write time.Duration `yaml:"write"`
|
||||||
|
Idle time.Duration `yaml:"idle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var configPath string
|
||||||
|
var runFunc = run
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Define and parse command-line flags.
|
||||||
|
flag.StringVar(&configPath, "config", "../../config/onix/adapter.yaml", "Path to the configuration file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Use custom log for initial setup messages.
|
||||||
|
log.Infof(context.Background(), "Starting application with config: %s", configPath)
|
||||||
|
|
||||||
|
// Run the application within a context.
|
||||||
|
if err := runFunc(context.Background(), configPath); err != nil {
|
||||||
|
log.Fatalf(context.Background(), err, "Application failed: %v", err)
|
||||||
|
}
|
||||||
|
log.Infof(context.Background(), "Application finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig loads and validates the configuration.
|
||||||
|
func initConfig(ctx context.Context, path string) (*Config, error) {
|
||||||
|
// Open the configuration file.
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open config file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Decode the YAML configuration.
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.NewDecoder(file).Decode(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not decode config: %w", err)
|
||||||
|
}
|
||||||
|
log.Debugf(ctx, "Read config: %#v", cfg)
|
||||||
|
// Validate the configuration.
|
||||||
|
if err := validateConfig(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateConfig validates the configuration.
|
||||||
|
func validateConfig(cfg *Config) error {
|
||||||
|
if strings.TrimSpace(cfg.AppName) == "" {
|
||||||
|
return fmt.Errorf("missing app name")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.HTTP.Port) == "" {
|
||||||
|
return fmt.Errorf("missing port")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newServer creates and initializes the HTTP server.
|
||||||
|
func newServer(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
err := module.Register(ctx, cfg.Modules, mux, mgr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to register modules: %w", err)
|
||||||
|
}
|
||||||
|
return mux, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var newManagerFunc = plugin.NewManager
|
||||||
|
var newServerFunc = newServer
|
||||||
|
|
||||||
|
// run encapsulates the application logic.
|
||||||
|
func run(ctx context.Context, configPath string) error {
|
||||||
|
closers := []func(){}
|
||||||
|
// Initialize configuration and logger.
|
||||||
|
cfg, err := initConfig(ctx, configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize config: %w", err)
|
||||||
|
}
|
||||||
|
log.Infof(ctx, "Initializing logger with config: %+v", cfg.Log)
|
||||||
|
if err := log.InitLogger(cfg.Log); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize logger: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize plugin manager.
|
||||||
|
log.Infof(ctx, "Initializing plugin manager")
|
||||||
|
mgr, closer, err := newManagerFunc(ctx, cfg.PluginManager)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create plugin manager: %w", err)
|
||||||
|
}
|
||||||
|
closers = append(closers, closer)
|
||||||
|
log.Debug(ctx, "Plugin manager loaded.")
|
||||||
|
|
||||||
|
// Initialize HTTP server.
|
||||||
|
log.Infof(ctx, "Initializing HTTP server")
|
||||||
|
srv, err := newServerFunc(ctx, mgr, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure HTTP server.
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: net.JoinHostPort("", cfg.HTTP.Port),
|
||||||
|
Handler: srv,
|
||||||
|
ReadTimeout: cfg.HTTP.Timeouts.Read * time.Second,
|
||||||
|
WriteTimeout: cfg.HTTP.Timeouts.Write * time.Second,
|
||||||
|
IdleTimeout: cfg.HTTP.Timeouts.Idle * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start HTTP server.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
log.Infof(ctx, "Server listening on %s", httpServer.Addr)
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Errorf(ctx, fmt.Errorf("http server ListenAndServe: %w", err), "error listening and serving")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Handle shutdown.
|
||||||
|
shutdown(ctx, httpServer, &wg, closers)
|
||||||
|
wg.Wait()
|
||||||
|
log.Infof(ctx, "Server shutdown complete")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown handles server shutdown.
|
||||||
|
func shutdown(ctx context.Context, httpServer *http.Server, wg *sync.WaitGroup, closers []func()) {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
<-ctx.Done()
|
||||||
|
log.Infof(ctx, "Shutting down server...")
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Errorf(ctx, fmt.Errorf("http server Shutdown: %w", err), "error shutting down http server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call all closer functions.
|
||||||
|
for _, closer := range closers {
|
||||||
|
closer()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
429
cmd/adapter/main_test.go
Normal file
429
cmd/adapter/main_test.go
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/core/module"
|
||||||
|
"github.com/beckn/beckn-onix/core/module/handler"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockPluginManager implements handler.PluginManager for testing.
|
||||||
|
type MockPluginManager struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns a middleware function based on the provided configuration.
|
||||||
|
func (m *MockPluginManager) Middleware(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignValidator returns a mock implementation of the Verifier interface.
|
||||||
|
func (m *MockPluginManager) SignValidator(ctx context.Context, cfg *plugin.Config) (definition.SignValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator returns a mock implementation of the SchemaValidator interface.
|
||||||
|
func (m *MockPluginManager) Validator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router returns a mock implementation of the Router interface.
|
||||||
|
func (m *MockPluginManager) Router(ctx context.Context, cfg *plugin.Config) (definition.Router, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publisher returns a mock implementation of the Publisher interface.
|
||||||
|
func (m *MockPluginManager) Publisher(ctx context.Context, cfg *plugin.Config) (definition.Publisher, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer returns a mock implementation of the Signer interface.
|
||||||
|
func (m *MockPluginManager) Signer(ctx context.Context, cfg *plugin.Config) (definition.Signer, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step returns a mock implementation of the Step interface.
|
||||||
|
func (m *MockPluginManager) Step(ctx context.Context, cfg *plugin.Config) (definition.Step, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache returns a mock implementation of the Cache interface.
|
||||||
|
func (m *MockPluginManager) Cache(ctx context.Context, cfg *plugin.Config) (definition.Cache, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyManager returns a mock implementation of the KeyManager interface.
|
||||||
|
func (m *MockPluginManager) KeyManager(ctx context.Context, cache definition.Cache, rLookup definition.RegistryLookup, cfg *plugin.Config) (definition.KeyManager, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaValidator returns a mock implementation of the SchemaValidator interface.
|
||||||
|
func (m *MockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockRun is a mock implementation of the `run` function, simulating a successful run.
|
||||||
|
func mockRun(ctx context.Context, configPath string) error {
|
||||||
|
return nil // Simulate a successful run
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMainFunction tests the main function execution, including command-line argument parsing.
|
||||||
|
func TestMainFunction(t *testing.T) {
|
||||||
|
// Backup original run function and restore it after test
|
||||||
|
origRun := runFunc
|
||||||
|
defer func() { runFunc = origRun }()
|
||||||
|
runFunc = mockRun
|
||||||
|
|
||||||
|
origArgs := os.Args
|
||||||
|
defer func() { os.Args = origArgs }()
|
||||||
|
|
||||||
|
// Set mock command-line arguments
|
||||||
|
os.Args = []string{"cmd", "-config=../../config/test-config.yaml"}
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("test", flag.ExitOnError)
|
||||||
|
fs.StringVar(&configPath, "config", "../../config/clientSideHandler-config.yaml", "Path to the configuration file")
|
||||||
|
|
||||||
|
if err := fs.Parse(os.Args[1:]); err != nil {
|
||||||
|
t.Fatalf("Failed to parse flags: %v", err)
|
||||||
|
}
|
||||||
|
main()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunSuccess(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
configPath := "../test/validConfig.yaml"
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
originalNewManager := newManagerFunc
|
||||||
|
newManagerFunc = func(ctx context.Context, cfg *plugin.ManagerConfig) (*plugin.Manager, func(), error) {
|
||||||
|
return &plugin.Manager{}, func() {}, nil
|
||||||
|
}
|
||||||
|
defer func() { newManagerFunc = originalNewManager }()
|
||||||
|
|
||||||
|
originalNewServer := newServerFunc
|
||||||
|
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
|
||||||
|
return http.NewServeMux(), nil
|
||||||
|
}
|
||||||
|
defer func() { newServerFunc = originalNewServer }()
|
||||||
|
|
||||||
|
if err := run(ctx, filepath.Clean(configPath)); err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunFailure validates failure scenarios for the run function.
|
||||||
|
func TestRunFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configData string
|
||||||
|
mockMgr func() (*MockPluginManager, func(), error)
|
||||||
|
mockLogger func(cfg *Config) error
|
||||||
|
mockServer func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error)
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid Config File",
|
||||||
|
configData: "invalid_config.yaml",
|
||||||
|
mockMgr: func() (*MockPluginManager, func(), error) {
|
||||||
|
return &MockPluginManager{}, func() {}, nil
|
||||||
|
},
|
||||||
|
mockLogger: func(cfg *Config) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
mockServer: func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
|
||||||
|
return nil, errors.New("failed to start server")
|
||||||
|
},
|
||||||
|
expectedErr: "failed to initialize config: invalid config: missing app name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
testFilePath := tt.configData
|
||||||
|
mockConfig := `invalid: "config"`
|
||||||
|
err := os.WriteFile(testFilePath, []byte(mockConfig), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to create test config file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(testFilePath)
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
originalNewManager := newManagerFunc
|
||||||
|
// newManagerFunc = func(ctx context.Context, cfg *plugin.ManagerConfig) (*plugin.Manager, func(), error) {
|
||||||
|
// return tt.mockMgr()
|
||||||
|
// }
|
||||||
|
newManagerFunc = nil
|
||||||
|
defer func() { newManagerFunc = originalNewManager }()
|
||||||
|
|
||||||
|
originalNewServer := newServerFunc
|
||||||
|
newServerFunc = func(ctx context.Context, mgr handler.PluginManager, cfg *Config) (http.Handler, error) {
|
||||||
|
return tt.mockServer(ctx, mgr, cfg)
|
||||||
|
}
|
||||||
|
defer func() { newServerFunc = originalNewServer }()
|
||||||
|
|
||||||
|
// Run function
|
||||||
|
err = run(ctx, testFilePath)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error, but got nil")
|
||||||
|
} else if err.Error() != tt.expectedErr {
|
||||||
|
t.Errorf("Expected error '%s', but got '%s'", tt.expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitConfigSuccess tests the successful initialization of the config.
|
||||||
|
func TestInitConfigSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configData string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Config",
|
||||||
|
configData: `
|
||||||
|
appName: "TestApp"
|
||||||
|
http:
|
||||||
|
port: "8080"
|
||||||
|
timeout:
|
||||||
|
read: 5
|
||||||
|
write: 5
|
||||||
|
idle: 10
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
configPath := "test_config_success.yaml"
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
|
||||||
|
err := os.WriteFile(configPath, []byte(tt.configData), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to create test config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = initConfig(context.Background(), configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitConfigFailure tests failure scenarios for config initialization.
|
||||||
|
func TestInitConfigFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configData string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid YAML Format",
|
||||||
|
configData: `appName: "TestApp"\nhttp: { invalid_yaml }`,
|
||||||
|
expectedErr: "could not decode config",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing Required Fields",
|
||||||
|
configData: `appName: ""\nhttp:\n timeout:\n read: 5\n`,
|
||||||
|
expectedErr: "could not decode config: yaml: did not find expected key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-Existent File",
|
||||||
|
configData: "",
|
||||||
|
expectedErr: "could not open config file",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
configPath := "test_config_failure.yaml"
|
||||||
|
|
||||||
|
if tt.configData != "" {
|
||||||
|
err := os.WriteFile(configPath, []byte(tt.configData), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to create test config file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(configPath)
|
||||||
|
} else {
|
||||||
|
// Ensure file does not exist for non-existent file test
|
||||||
|
os.Remove(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := initConfig(context.Background(), configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error but got nil")
|
||||||
|
} else if !strings.Contains(err.Error(), tt.expectedErr) {
|
||||||
|
t.Errorf("Expected error containing '%s', but got '%s'", tt.expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewServerSuccess tests successful server creation.
|
||||||
|
func TestNewServerSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modules []module.Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Successful server creation with no modules",
|
||||||
|
modules: []module.Config{}, // No modules to simplify the test
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockMgr := new(MockPluginManager) // Mocking PluginManager
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
Modules: tt.modules,
|
||||||
|
HTTP: httpConfig{
|
||||||
|
Port: "8080",
|
||||||
|
Timeouts: timeoutConfig{
|
||||||
|
Read: 5,
|
||||||
|
Write: 5,
|
||||||
|
Idle: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, err := newServer(context.Background(), mockMgr, cfg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
if handler == nil {
|
||||||
|
t.Errorf("Expected handler to be non-nil, but got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewServerFailure tests failure scenarios when creating a server.
|
||||||
|
func TestNewServerFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
modules []module.Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Module registration failure",
|
||||||
|
modules: []module.Config{
|
||||||
|
{
|
||||||
|
Name: "InvalidModule",
|
||||||
|
Path: "/invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockMgr := new(MockPluginManager) // Mocking PluginManager
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
Modules: tt.modules,
|
||||||
|
HTTP: httpConfig{
|
||||||
|
Port: "8080",
|
||||||
|
Timeouts: timeoutConfig{
|
||||||
|
Read: 5,
|
||||||
|
Write: 5,
|
||||||
|
Idle: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler, err := newServer(context.Background(), mockMgr, cfg)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error, but got nil")
|
||||||
|
}
|
||||||
|
if handler != nil {
|
||||||
|
t.Errorf("Expected handler to be nil, but got a non-nil value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidateConfigSuccess tests validation of a correct config.
|
||||||
|
func TestValidateConfigSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg Config
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid Config",
|
||||||
|
cfg: Config{
|
||||||
|
AppName: "TestApp",
|
||||||
|
HTTP: httpConfig{
|
||||||
|
Port: "8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateConfig(&tt.cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidateConfigFailure tests validation failures for incorrect config.
|
||||||
|
func TestValidateConfigFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg Config
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Missing AppName",
|
||||||
|
cfg: Config{
|
||||||
|
AppName: "",
|
||||||
|
HTTP: httpConfig{
|
||||||
|
Port: "8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: "missing app name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing Port",
|
||||||
|
cfg: Config{
|
||||||
|
AppName: "TestApp",
|
||||||
|
HTTP: httpConfig{
|
||||||
|
Port: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: "missing port",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateConfig(&tt.cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error '%s', but got nil", tt.expectedErr)
|
||||||
|
} else if err.Error() != tt.expectedErr {
|
||||||
|
t.Errorf("Expected error '%s', but got '%s'", tt.expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
37
cmd/test/validConfig.yaml
Normal file
37
cmd/test/validConfig.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
appName: "testAdapter"
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
destinations:
|
||||||
|
- type: stdout
|
||||||
|
context_keys:
|
||||||
|
- transaction_id
|
||||||
|
- message_id
|
||||||
|
http:
|
||||||
|
port: 8080
|
||||||
|
timeout:
|
||||||
|
read: 30
|
||||||
|
write: 30
|
||||||
|
idle: 30
|
||||||
|
plugin:
|
||||||
|
root: "/mock/plugins"
|
||||||
|
pluginZipPath: "/mock/plugins/plugins_bundle.zip"
|
||||||
|
plugins:
|
||||||
|
- testPlugin1
|
||||||
|
- testPlugin2
|
||||||
|
modules:
|
||||||
|
- name: testModule
|
||||||
|
type: transaction
|
||||||
|
path: /testPath
|
||||||
|
targetType: msgQ
|
||||||
|
plugin:
|
||||||
|
schemaValidator:
|
||||||
|
id: testValidator
|
||||||
|
publisher:
|
||||||
|
id: testPublisher
|
||||||
|
config:
|
||||||
|
project: test-project
|
||||||
|
topic: test-topic
|
||||||
|
router:
|
||||||
|
id: testRouter
|
||||||
|
config:
|
||||||
|
routingConfigPath: "/mock/configs/testRouting-config.yaml"
|
||||||
98
config/onix-bap/adapter.yaml
Normal file
98
config/onix-bap/adapter.yaml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
appName: "onix"
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
destinations:
|
||||||
|
- type: stdout
|
||||||
|
contextKeys:
|
||||||
|
- transaction_id
|
||||||
|
- message_id
|
||||||
|
- subscriber_id
|
||||||
|
- module_id
|
||||||
|
http:
|
||||||
|
port: 8080
|
||||||
|
timeout:
|
||||||
|
read: 30
|
||||||
|
write: 30
|
||||||
|
idle: 30
|
||||||
|
pluginManager:
|
||||||
|
root: /app/plugins
|
||||||
|
remoteRoot: /mnt/gcs/plugins/plugins_bundle.zip
|
||||||
|
modules:
|
||||||
|
- name: bapTxnReciever
|
||||||
|
path: /bap/reciever/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bap
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signValidator:
|
||||||
|
id: signvalidator
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bapTxnReciever-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bap
|
||||||
|
steps:
|
||||||
|
- validateSign
|
||||||
|
- addRoute
|
||||||
|
- validateSchema
|
||||||
|
- name: bapTxnCaller
|
||||||
|
path: /bap/caller/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
role: bap
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signer:
|
||||||
|
id: signer
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bapTxnCaller-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bap
|
||||||
|
steps:
|
||||||
|
- validateSchema
|
||||||
|
- addRoute
|
||||||
|
- sign
|
||||||
25
config/onix-bap/bapTxnCaller-routing.yaml
Normal file
25
config/onix-bap/bapTxnCaller-routing.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
target:
|
||||||
|
url: "https://gateway.example.com"
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
- domain: "ONDC:TRV12"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
20
config/onix-bap/bapTxnReciever-routing.yaml
Normal file
20
config/onix-bap/bapTxnReciever-routing.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "msgq"
|
||||||
|
target:
|
||||||
|
topic_id: "trv_topic_id1"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
8
config/onix-bap/plugin.yaml
Normal file
8
config/onix-bap/plugin.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
plugins:
|
||||||
|
- gcpAuthMdw
|
||||||
|
- nopsigner
|
||||||
|
- router
|
||||||
|
- publisher
|
||||||
|
- reqpreprocessor
|
||||||
|
- nopschemavalidator
|
||||||
|
- nopsignvalidator
|
||||||
99
config/onix-bpp/adapter.yaml
Normal file
99
config/onix-bpp/adapter.yaml
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
appName: "onix"
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
destinations:
|
||||||
|
- type: stdout
|
||||||
|
contextKeys:
|
||||||
|
- transaction_id
|
||||||
|
- message_id
|
||||||
|
- subscriber_id
|
||||||
|
- module_id
|
||||||
|
http:
|
||||||
|
port: 8080
|
||||||
|
timeout:
|
||||||
|
read: 30
|
||||||
|
write: 30
|
||||||
|
idle: 30
|
||||||
|
pluginManager:
|
||||||
|
root: /app/plugins
|
||||||
|
remoteRoot: /mnt/gcs/plugins/plugins_bundle.zip
|
||||||
|
modules:
|
||||||
|
- name: bppTxnReciever
|
||||||
|
path: /bpp/reciever/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bpp
|
||||||
|
subscriberId: bpp1
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signValidator:
|
||||||
|
id: signvalidator
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bppTxnReciever-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bpp
|
||||||
|
steps:
|
||||||
|
- validateSign
|
||||||
|
- addRoute
|
||||||
|
- validateSchema
|
||||||
|
- name: bppTxnCaller
|
||||||
|
path: /bpp/caller/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bpp
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signer:
|
||||||
|
id: signer
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bppTxnCaller-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bpp
|
||||||
|
steps:
|
||||||
|
- validateSchema
|
||||||
|
- addRoute
|
||||||
|
- sign
|
||||||
23
config/onix-bpp/bppTxnCaller-routing.yaml
Normal file
23
config/onix-bpp/bppTxnCaller-routing.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bap"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: "ONDC:TRV11"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bap"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
30
config/onix-bpp/bppTxnReciever-routing.yaml
Normal file
30
config/onix-bpp/bppTxnReciever-routing.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "msgq"
|
||||||
|
target:
|
||||||
|
topic_id: "trv_topic_id1"
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
|
||||||
|
- domain: "ONDC:TRV11"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
8
config/onix-bpp/plugin.yaml
Normal file
8
config/onix-bpp/plugin.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
plugins:
|
||||||
|
- gcpAuthMdw
|
||||||
|
- nopsigner
|
||||||
|
- router
|
||||||
|
- publisher
|
||||||
|
- reqpreprocessor
|
||||||
|
- nopschemavalidator
|
||||||
|
- nopsignvalidator
|
||||||
177
config/onix/adapter.yaml
Normal file
177
config/onix/adapter.yaml
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
appName: "onix"
|
||||||
|
log:
|
||||||
|
level: debug
|
||||||
|
destinations:
|
||||||
|
- type: stdout
|
||||||
|
contextKeys:
|
||||||
|
- transaction_id
|
||||||
|
- message_id
|
||||||
|
- subscriber_id
|
||||||
|
- module_id
|
||||||
|
http:
|
||||||
|
port: 8080
|
||||||
|
timeout:
|
||||||
|
read: 30
|
||||||
|
write: 30
|
||||||
|
idle: 30
|
||||||
|
pluginManager:
|
||||||
|
root: /app/plugins
|
||||||
|
remoteRoot: /mnt/gcs/plugins/plugins_bundle.zip
|
||||||
|
modules:
|
||||||
|
- name: bapTxnReciever
|
||||||
|
path: /bap/reciever/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bap
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 10.81.192.4:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signValidator:
|
||||||
|
id: signvalidator
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bapTxnReciever-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bap
|
||||||
|
steps:
|
||||||
|
- validateSign
|
||||||
|
- addRoute
|
||||||
|
- validateSchema
|
||||||
|
- name: bapTxnCaller
|
||||||
|
path: /bap/caller/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
role: bap
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signer:
|
||||||
|
id: signer
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bapTxnCaller-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bap
|
||||||
|
steps:
|
||||||
|
- validateSchema
|
||||||
|
- addRoute
|
||||||
|
- sign
|
||||||
|
- name: bppTxnReciever
|
||||||
|
path: /bpp/reciever/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bpp
|
||||||
|
subscriberId: bpp1
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signValidator:
|
||||||
|
id: signvalidator
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bppTxnReciever-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bpp
|
||||||
|
steps:
|
||||||
|
- validateSign
|
||||||
|
- addRoute
|
||||||
|
- validateSchema
|
||||||
|
- name: bppTxnCaller
|
||||||
|
path: /bpp/caller/
|
||||||
|
handler:
|
||||||
|
type: std
|
||||||
|
role: bpp
|
||||||
|
registryUrl: http://localhost:8080/reg
|
||||||
|
plugins:
|
||||||
|
keyManager:
|
||||||
|
id: secretskeymanager
|
||||||
|
config:
|
||||||
|
projectID: ${projectID}
|
||||||
|
cache:
|
||||||
|
id: redis
|
||||||
|
config:
|
||||||
|
addr: 192.168.1.1:6379
|
||||||
|
schemaValidator:
|
||||||
|
id: schemavalidator
|
||||||
|
config:
|
||||||
|
schemaDir: /mnt/gcs/configs/schemas
|
||||||
|
signer:
|
||||||
|
id: signer
|
||||||
|
publisher:
|
||||||
|
id: publisher
|
||||||
|
config:
|
||||||
|
project: ${projectID}
|
||||||
|
topic: bapNetworkReciever
|
||||||
|
router:
|
||||||
|
id: router
|
||||||
|
config:
|
||||||
|
routingConfigPath: /mnt/gcs/configs/bppTxnCaller-routing.yaml
|
||||||
|
middleware:
|
||||||
|
- id: reqpreprocessor
|
||||||
|
config:
|
||||||
|
uuidKeys: transaction_id,message_id
|
||||||
|
role: bpp
|
||||||
|
steps:
|
||||||
|
- validateSchema
|
||||||
|
- addRoute
|
||||||
|
- sign
|
||||||
25
config/onix/bapTxnCaller-routing.yaml
Normal file
25
config/onix/bapTxnCaller-routing.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
target:
|
||||||
|
url: "https://gateway.example.com"
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
- domain: "ONDC:TRV12"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bpp"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
20
config/onix/bapTxnReciever-routing.yaml
Normal file
20
config/onix/bapTxnReciever-routing.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "msgq"
|
||||||
|
target:
|
||||||
|
topic_id: "trv_topic_id1"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
23
config/onix/bppTxnCaller-routing.yaml
Normal file
23
config/onix/bppTxnCaller-routing.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bap"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: "ONDC:TRV11"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "bap"
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
30
config/onix/bppTxnReciever-routing.yaml
Normal file
30
config/onix/bppTxnReciever-routing.yaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
|
||||||
|
- domain: "ONDC:TRV10"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "msgq"
|
||||||
|
target:
|
||||||
|
topic_id: "trv_topic_id1"
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
|
||||||
|
- domain: "ONDC:TRV11"
|
||||||
|
version: "2.0.0"
|
||||||
|
routingType: "url"
|
||||||
|
target:
|
||||||
|
url: "https://services-backend/trv/v1"
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
8
config/onix/plugin.yaml
Normal file
8
config/onix/plugin.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
plugins:
|
||||||
|
- gcpAuthMdw
|
||||||
|
- nopsigner
|
||||||
|
- router
|
||||||
|
- publisher
|
||||||
|
- reqpreprocessor
|
||||||
|
- nopschemavalidator
|
||||||
|
- nopsignvalidator
|
||||||
101
core/module/client/registery.go
Normal file
101
core/module/client/registery.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config struct to hold configuration parameters.
|
||||||
|
type Config struct {
|
||||||
|
RegisteryURL string
|
||||||
|
RetryMax int
|
||||||
|
RetryWaitMin time.Duration
|
||||||
|
RetryWaitMax time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// registryClient encapsulates the logic for calling the subscribe and lookup endpoints.
|
||||||
|
type registryClient struct {
|
||||||
|
config *Config
|
||||||
|
client *retryablehttp.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRegisteryClient creates a new instance of Client.
|
||||||
|
func NewRegisteryClient(config *Config) *registryClient {
|
||||||
|
retryClient := retryablehttp.NewClient()
|
||||||
|
|
||||||
|
return ®istryClient{config: config, client: retryClient}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe calls the /subscribe endpoint with retry.
|
||||||
|
func (c *registryClient) Subscribe(ctx context.Context, subscription *model.Subscription) error {
|
||||||
|
subscribeURL := fmt.Sprintf("%s/subscribe", c.config.RegisteryURL)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(subscription)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewBadReqErr(fmt.Errorf("failed to marshal subscription data: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := retryablehttp.NewRequest("POST", subscribeURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send request with retry: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("subscribe request failed with status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup calls the /lookup endpoint with retry and returns a slice of Subscription.
|
||||||
|
func (c *registryClient) Lookup(ctx context.Context, subscription *model.Subscription) ([]model.Subscription, error) {
|
||||||
|
lookupURL := fmt.Sprintf("%s/lookUp", c.config.RegisteryURL)
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(subscription)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewBadReqErr(fmt.Errorf("failed to marshal subscription data: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := retryablehttp.NewRequest("POST", lookupURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to send request with retry: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("lookup request failed with status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []model.Subscription
|
||||||
|
err = json.Unmarshal(body, &results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
231
core/module/client/registry_test.go
Normal file
231
core/module/client/registry_test.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSubscribeSuccess verifies that the Subscribe function succeeds when the server responds with HTTP 200.
|
||||||
|
func TestSubscribeSuccess(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
if _, err := w.Write([]byte("{}")); err != nil {
|
||||||
|
t.Errorf("failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewRegisteryClient(&Config{
|
||||||
|
RegisteryURL: server.URL,
|
||||||
|
RetryMax: 3,
|
||||||
|
RetryWaitMin: time.Millisecond * 100,
|
||||||
|
RetryWaitMax: time.Millisecond * 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
KeyID: "test-key",
|
||||||
|
SigningPublicKey: "test-signing-key",
|
||||||
|
EncrPublicKey: "test-encryption-key",
|
||||||
|
ValidFrom: time.Now(),
|
||||||
|
ValidUntil: time.Now().Add(24 * time.Hour),
|
||||||
|
Status: "SUBSCRIBED",
|
||||||
|
}
|
||||||
|
err := client.Subscribe(context.Background(), subscription)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Subscribe() failed with error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSubscribeFailure tests different failure scenarios using a mock client.
|
||||||
|
func TestSubscribeFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mockError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Failed subscription - Internal Server Error",
|
||||||
|
mockError: errors.New("internal server error"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed subscription - Bad Request",
|
||||||
|
mockError: errors.New("bad request"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Request Timeout",
|
||||||
|
mockError: context.DeadlineExceeded,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Network Failure",
|
||||||
|
mockError: errors.New("network failure"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON Marshalling Failure",
|
||||||
|
mockError: errors.New("json marshalling failure"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client := NewRegisteryClient(&Config{
|
||||||
|
RetryMax: 1,
|
||||||
|
RetryWaitMin: 1 * time.Millisecond,
|
||||||
|
RetryWaitMax: 2 * time.Millisecond,
|
||||||
|
})
|
||||||
|
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
KeyID: "test-key",
|
||||||
|
SigningPublicKey: "test-signing-key",
|
||||||
|
EncrPublicKey: "test-encryption-key",
|
||||||
|
ValidFrom: time.Now(),
|
||||||
|
ValidUntil: time.Now().Add(24 * time.Hour),
|
||||||
|
Status: "SUBSCRIBED",
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.name == "JSON Marshalling Failure" {
|
||||||
|
subscription = &model.Subscription{} // Example of an invalid object
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Subscribe(context.Background(), subscription)
|
||||||
|
require.Error(t, err) // Directly checking for an error since all cases should fail
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLookupSuccess tests successful lookup scenarios.
|
||||||
|
func TestLookupSuccess(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
response := []model.Subscription{
|
||||||
|
{
|
||||||
|
Subscriber: model.Subscriber{
|
||||||
|
SubscriberID: "123",
|
||||||
|
},
|
||||||
|
KeyID: "test-key",
|
||||||
|
SigningPublicKey: "test-signing-key",
|
||||||
|
EncrPublicKey: "test-encryption-key",
|
||||||
|
ValidFrom: time.Now(),
|
||||||
|
ValidUntil: time.Now().Add(24 * time.Hour),
|
||||||
|
Status: "SUBSCRIBED",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(response)
|
||||||
|
if _, err := w.Write(bodyBytes); err != nil {
|
||||||
|
t.Errorf("failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
RegisteryURL: server.URL,
|
||||||
|
RetryMax: 1,
|
||||||
|
RetryWaitMin: 1 * time.Millisecond,
|
||||||
|
RetryWaitMax: 2 * time.Millisecond,
|
||||||
|
}
|
||||||
|
rClient := NewRegisteryClient(config)
|
||||||
|
ctx := context.Background()
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
Subscriber: model.Subscriber{
|
||||||
|
SubscriberID: "123",
|
||||||
|
},
|
||||||
|
KeyID: "test-key",
|
||||||
|
SigningPublicKey: "test-signing-key",
|
||||||
|
EncrPublicKey: "test-encryption-key",
|
||||||
|
ValidFrom: time.Now(),
|
||||||
|
ValidUntil: time.Now().Add(24 * time.Hour),
|
||||||
|
Status: "SUBSCRIBED",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := rClient.Lookup(ctx, subscription)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, result)
|
||||||
|
require.Equal(t, subscription.Subscriber.SubscriberID, result[0].Subscriber.SubscriberID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLookupFailure tests failure scenarios for the Lookup function.
|
||||||
|
func TestLookupFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
responseBody interface{}
|
||||||
|
responseCode int
|
||||||
|
setupMock func(*httptest.Server)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Lookup failure - non 200 status",
|
||||||
|
responseBody: "Internal Server Error",
|
||||||
|
responseCode: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid JSON response",
|
||||||
|
responseBody: "Invalid JSON",
|
||||||
|
responseCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Server timeout",
|
||||||
|
setupMock: func(server *httptest.Server) {
|
||||||
|
server.Config.WriteTimeout = 1 * time.Millisecond // Force timeout
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty response body",
|
||||||
|
responseBody: "",
|
||||||
|
responseCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if tc.responseCode != 0 { // Prevent WriteHeader(0) error
|
||||||
|
w.WriteHeader(tc.responseCode)
|
||||||
|
}
|
||||||
|
if tc.responseBody != nil {
|
||||||
|
if str, ok := tc.responseBody.(string); ok {
|
||||||
|
if _, err := w.Write([]byte(str)); err != nil {
|
||||||
|
t.Errorf("failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyBytes, _ := json.Marshal(tc.responseBody)
|
||||||
|
if _, err := w.Write(bodyBytes); err != nil {
|
||||||
|
t.Errorf("failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if tc.setupMock != nil {
|
||||||
|
tc.setupMock(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
RegisteryURL: server.URL,
|
||||||
|
RetryMax: 0,
|
||||||
|
RetryWaitMin: 1 * time.Millisecond,
|
||||||
|
RetryWaitMax: 2 * time.Millisecond,
|
||||||
|
}
|
||||||
|
rClient := NewRegisteryClient(config)
|
||||||
|
ctx := context.Background()
|
||||||
|
subscription := &model.Subscription{
|
||||||
|
Subscriber: model.Subscriber{},
|
||||||
|
KeyID: "test-key",
|
||||||
|
SigningPublicKey: "test-signing-key",
|
||||||
|
EncrPublicKey: "test-encryption-key",
|
||||||
|
ValidFrom: time.Now(),
|
||||||
|
ValidUntil: time.Now().Add(24 * time.Hour),
|
||||||
|
Status: "SUBSCRIBED",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := rClient.Lookup(ctx, subscription)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Empty(t, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
55
core/module/handler/config.go
Normal file
55
core/module/handler/config.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginManager defines an interface for managing plugins dynamically.
|
||||||
|
type PluginManager interface {
|
||||||
|
Middleware(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error)
|
||||||
|
SignValidator(ctx context.Context, cfg *plugin.Config) (definition.SignValidator, error)
|
||||||
|
Validator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error)
|
||||||
|
Router(ctx context.Context, cfg *plugin.Config) (definition.Router, error)
|
||||||
|
Publisher(ctx context.Context, cfg *plugin.Config) (definition.Publisher, error)
|
||||||
|
Signer(ctx context.Context, cfg *plugin.Config) (definition.Signer, error)
|
||||||
|
Step(ctx context.Context, cfg *plugin.Config) (definition.Step, error)
|
||||||
|
Cache(ctx context.Context, cfg *plugin.Config) (definition.Cache, error)
|
||||||
|
KeyManager(ctx context.Context, cache definition.Cache, rLookup definition.RegistryLookup, cfg *plugin.Config) (definition.KeyManager, error)
|
||||||
|
SchemaValidator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type defines different handler types for processing requests.
|
||||||
|
type Type string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HandlerTypeStd represents the standard handler type used for general request processing.
|
||||||
|
HandlerTypeStd Type = "std"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PluginCfg holds the configuration for various plugins.
|
||||||
|
type PluginCfg struct {
|
||||||
|
SchemaValidator *plugin.Config `yaml:"schemaValidator,omitempty"`
|
||||||
|
SignValidator *plugin.Config `yaml:"signValidator,omitempty"`
|
||||||
|
Publisher *plugin.Config `yaml:"publisher,omitempty"`
|
||||||
|
Signer *plugin.Config `yaml:"signer,omitempty"`
|
||||||
|
Router *plugin.Config `yaml:"router,omitempty"`
|
||||||
|
Cache *plugin.Config `yaml:"cache,omitempty"`
|
||||||
|
KeyManager *plugin.Config `yaml:"keyManager,omitempty"`
|
||||||
|
Middleware []plugin.Config `yaml:"middleware,omitempty"`
|
||||||
|
Steps []plugin.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds the configuration for request processing handlers.
|
||||||
|
type Config struct {
|
||||||
|
Plugins PluginCfg `yaml:"plugins"`
|
||||||
|
Steps []string
|
||||||
|
Type Type
|
||||||
|
RegistryURL string `yaml:"registryUrl"`
|
||||||
|
Role model.Role
|
||||||
|
SubscriberID string `yaml:"subscriberId"`
|
||||||
|
}
|
||||||
264
core/module/handler/stdHandler.go
Normal file
264
core/module/handler/stdHandler.go
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/core/module/client"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stdHandler orchestrates the execution of defined processing steps.
|
||||||
|
type stdHandler struct {
|
||||||
|
signer definition.Signer
|
||||||
|
steps []definition.Step
|
||||||
|
signValidator definition.SignValidator
|
||||||
|
cache definition.Cache
|
||||||
|
km definition.KeyManager
|
||||||
|
schemaValidator definition.SchemaValidator
|
||||||
|
router definition.Router
|
||||||
|
publisher definition.Publisher
|
||||||
|
SubscriberID string
|
||||||
|
role model.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStdHandler initializes a new processor with plugins and steps.
|
||||||
|
func NewStdHandler(ctx context.Context, mgr PluginManager, cfg *Config) (http.Handler, error) {
|
||||||
|
h := &stdHandler{
|
||||||
|
steps: []definition.Step{},
|
||||||
|
SubscriberID: cfg.SubscriberID,
|
||||||
|
role: cfg.Role,
|
||||||
|
}
|
||||||
|
// Initialize plugins.
|
||||||
|
if err := h.initPlugins(ctx, mgr, &cfg.Plugins, cfg.RegistryURL); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize plugins: %w", err)
|
||||||
|
}
|
||||||
|
// Initialize steps.
|
||||||
|
if err := h.initSteps(ctx, mgr, cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize steps: %w", err)
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP processes an incoming HTTP request and executes defined processing steps.
|
||||||
|
func (h *stdHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, err := h.stepCtx(r, w.Header())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(r.Context(), err, "stepCtx(r):%v", err)
|
||||||
|
response.SendNack(r.Context(), w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Request(r.Context(), r, ctx.Body)
|
||||||
|
|
||||||
|
// Execute processing steps.
|
||||||
|
for _, step := range h.steps {
|
||||||
|
if err := step.Run(ctx); err != nil {
|
||||||
|
log.Errorf(ctx, err, "%T.run(%v):%v", step, ctx, err)
|
||||||
|
response.SendNack(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore request body before forwarding or publishing.
|
||||||
|
r.Body = io.NopCloser(bytes.NewReader(ctx.Body))
|
||||||
|
if ctx.Route == nil {
|
||||||
|
response.SendAck(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle routing based on the defined route type.
|
||||||
|
route(ctx, r, w, h.publisher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stepCtx creates a new StepContext for processing an HTTP request.
|
||||||
|
func (h *stdHandler) stepCtx(r *http.Request, rh http.Header) (*model.StepContext, error) {
|
||||||
|
var bodyBuffer bytes.Buffer
|
||||||
|
if _, err := io.Copy(&bodyBuffer, r.Body); err != nil {
|
||||||
|
return nil, model.NewBadReqErr(err)
|
||||||
|
}
|
||||||
|
r.Body.Close()
|
||||||
|
subID := h.subID(r.Context())
|
||||||
|
if len(subID) == 0 {
|
||||||
|
return nil, model.NewBadReqErr(fmt.Errorf("subscriberID not set"))
|
||||||
|
}
|
||||||
|
return &model.StepContext{
|
||||||
|
Context: r.Context(),
|
||||||
|
Request: r,
|
||||||
|
Body: bodyBuffer.Bytes(),
|
||||||
|
Role: h.role,
|
||||||
|
SubID: subID,
|
||||||
|
RespHeader: rh,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// subID retrieves the subscriber ID from the request context.
|
||||||
|
func (h *stdHandler) subID(ctx context.Context) string {
|
||||||
|
rSubID, ok := ctx.Value(model.ContextKeySubscriberID).(string)
|
||||||
|
if ok {
|
||||||
|
return rSubID
|
||||||
|
}
|
||||||
|
return h.SubscriberID
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyFunc = proxy
|
||||||
|
|
||||||
|
// route handles request forwarding or message publishing based on the routing type.
|
||||||
|
func route(ctx *model.StepContext, r *http.Request, w http.ResponseWriter, pb definition.Publisher) {
|
||||||
|
log.Debugf(ctx, "Routing to ctx.Route to %#v", ctx.Route)
|
||||||
|
switch ctx.Route.TargetType {
|
||||||
|
case "url":
|
||||||
|
log.Infof(ctx.Context, "Forwarding request to URL: %s", ctx.Route.URL)
|
||||||
|
proxyFunc(r, w, ctx.Route.URL)
|
||||||
|
return
|
||||||
|
case "publisher":
|
||||||
|
if pb == nil {
|
||||||
|
err := fmt.Errorf("publisher plugin not configured")
|
||||||
|
log.Errorf(ctx.Context, err, "Invalid configuration:%v", err)
|
||||||
|
response.SendNack(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof(ctx.Context, "Publishing message to: %s", ctx.Route.PublisherID)
|
||||||
|
if err := pb.Publish(ctx, ctx.Route.PublisherID, ctx.Body); err != nil {
|
||||||
|
log.Errorf(ctx.Context, err, "Failed to publish message")
|
||||||
|
http.Error(w, "Error publishing message", http.StatusInternalServerError)
|
||||||
|
response.SendNack(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err := fmt.Errorf("unknown route type: %s", ctx.Route.TargetType)
|
||||||
|
log.Errorf(ctx.Context, err, "Invalid configuration:%v", err)
|
||||||
|
response.SendNack(ctx, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.SendAck(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy forwards the request to a target URL using a reverse proxy.
|
||||||
|
func proxy(r *http.Request, w http.ResponseWriter, target *url.URL) {
|
||||||
|
r.URL.Scheme = target.Scheme
|
||||||
|
r.URL.Host = target.Host
|
||||||
|
r.URL.Path = target.Path
|
||||||
|
|
||||||
|
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||||
|
log.Infof(r.Context(), "Proxying request to: %s", target)
|
||||||
|
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadPlugin is a generic function to load and validate plugins.
|
||||||
|
func loadPlugin[T any](ctx context.Context, name string, cfg *plugin.Config, mgrFunc func(context.Context, *plugin.Config) (T, error)) (T, error) {
|
||||||
|
var zero T
|
||||||
|
if cfg == nil {
|
||||||
|
log.Debugf(ctx, "Skipping %s plugin: not configured", name)
|
||||||
|
return zero, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin, err := mgrFunc(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("failed to load %s plugin (%s): %w", name, cfg.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf(ctx, "Loaded %s plugin: %s", name, cfg.ID)
|
||||||
|
return plugin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadKeyManager loads the KeyManager plugin using the provided PluginManager, cache, and registry URL.
|
||||||
|
func loadKeyManager(ctx context.Context, mgr PluginManager, cache definition.Cache, cfg *plugin.Config, regURL string) (definition.KeyManager, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
log.Debug(ctx, "Skipping KeyManager plugin: not configured")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if cache == nil {
|
||||||
|
return nil, fmt.Errorf("failed to load KeyManager plugin (%s): Cache plugin not configured", cfg.ID)
|
||||||
|
}
|
||||||
|
rClient := client.NewRegisteryClient(&client.Config{RegisteryURL: regURL})
|
||||||
|
km, err := mgr.KeyManager(ctx, cache, rClient, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load cache plugin (%s): %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf(ctx, "Loaded Keymanager plugin: %s", cfg.ID)
|
||||||
|
return km, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initPlugins initializes required plugins for the processor.
|
||||||
|
func (h *stdHandler) initPlugins(ctx context.Context, mgr PluginManager, cfg *PluginCfg, regURL string) error {
|
||||||
|
var err error
|
||||||
|
if h.cache, err = loadPlugin(ctx, "Cache", cfg.Cache, mgr.Cache); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.km, err = loadKeyManager(ctx, mgr, h.cache, cfg.KeyManager, regURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.signValidator, err = loadPlugin(ctx, "SignValidator", cfg.SignValidator, mgr.SignValidator); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.schemaValidator, err = loadPlugin(ctx, "SchemaValidator", cfg.SchemaValidator, mgr.SchemaValidator); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.router, err = loadPlugin(ctx, "Router", cfg.Router, mgr.Router); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.publisher, err = loadPlugin(ctx, "Publisher", cfg.Publisher, mgr.Publisher); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h.signer, err = loadPlugin(ctx, "Signer", cfg.Signer, mgr.Signer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf(ctx, "All required plugins successfully loaded for stdHandler")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSteps initializes and validates processing steps for the processor.
|
||||||
|
func (h *stdHandler) initSteps(ctx context.Context, mgr PluginManager, cfg *Config) error {
|
||||||
|
steps := make(map[string]definition.Step)
|
||||||
|
|
||||||
|
// Load plugin-based steps
|
||||||
|
for _, c := range cfg.Plugins.Steps {
|
||||||
|
step, err := mgr.Step(ctx, &c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize plugin step %s: %w", c.ID, err)
|
||||||
|
}
|
||||||
|
steps[c.ID] = step
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register processing steps
|
||||||
|
for _, step := range cfg.Steps {
|
||||||
|
var s definition.Step
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch step {
|
||||||
|
case "sign":
|
||||||
|
s, err = newSignStep(h.signer, h.km)
|
||||||
|
case "validateSign":
|
||||||
|
s, err = newValidateSignStep(h.signValidator, h.km)
|
||||||
|
case "validateSchema":
|
||||||
|
s, err = newValidateSchemaStep(h.schemaValidator)
|
||||||
|
case "addRoute":
|
||||||
|
s, err = newAddRouteStep(h.router)
|
||||||
|
default:
|
||||||
|
if customStep, exists := steps[step]; exists {
|
||||||
|
s = customStep
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unrecognized step: %s", step)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.steps = append(h.steps, s)
|
||||||
|
}
|
||||||
|
log.Infof(ctx, "Processor steps initialized: %v", cfg.Steps)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
169
core/module/handler/step.go
Normal file
169
core/module/handler/step.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
// signStep represents the signing step in the processing pipeline.
|
||||||
|
type signStep struct {
|
||||||
|
signer definition.Signer
|
||||||
|
km definition.KeyManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSignStep initializes and returns a new signing step.
|
||||||
|
func newSignStep(signer definition.Signer, km definition.KeyManager) (definition.Step, error) {
|
||||||
|
if signer == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: Signer plugin not configured")
|
||||||
|
}
|
||||||
|
if km == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: KeyManager plugin not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &signStep{signer: signer, km: km}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the signing step.
|
||||||
|
func (s *signStep) Run(ctx *model.StepContext) error {
|
||||||
|
keyID, key, err := s.km.SigningPrivateKey(ctx, ctx.SubID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get signing key: %w", err)
|
||||||
|
}
|
||||||
|
createdAt := time.Now().Unix()
|
||||||
|
validTill := time.Now().Add(5 * time.Minute).Unix()
|
||||||
|
sign, err := s.signer.Sign(ctx, ctx.Body, key, createdAt, validTill)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := s.generateAuthHeader(ctx.SubID, keyID, createdAt, validTill, sign)
|
||||||
|
|
||||||
|
header := model.AuthHeaderSubscriber
|
||||||
|
if ctx.Role == model.RoleGateway {
|
||||||
|
header = model.AuthHeaderGateway
|
||||||
|
}
|
||||||
|
ctx.Request.Header.Set(header, authHeader)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateAuthHeader constructs the authorization header for the signed request.
|
||||||
|
// It includes key ID, algorithm, creation time, expiration time, required headers, and signature.
|
||||||
|
func (s *signStep) generateAuthHeader(subID, keyID string, createdAt, validTill int64, signature string) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Signature keyId=\"%s|%s|ed25519\",algorithm=\"ed25519\",created=\"%d\",expires=\"%d\",headers=\"(created) (expires) digest\",signature=\"%s\"",
|
||||||
|
subID, keyID, createdAt, validTill, signature,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSignStep represents the signature validation step.
|
||||||
|
type validateSignStep struct {
|
||||||
|
validator definition.SignValidator
|
||||||
|
km definition.KeyManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValidateSignStep initializes and returns a new validate sign step.
|
||||||
|
func newValidateSignStep(signValidator definition.SignValidator, km definition.KeyManager) (definition.Step, error) {
|
||||||
|
if signValidator == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: SignValidator plugin not configured")
|
||||||
|
}
|
||||||
|
if km == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: KeyManager plugin not configured")
|
||||||
|
}
|
||||||
|
return &validateSignStep{validator: signValidator, km: km}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the validation step.
|
||||||
|
func (s *validateSignStep) Run(ctx *model.StepContext) error {
|
||||||
|
unauthHeader := fmt.Sprintf("Signature realm=\"%s\",headers=\"(created) (expires) digest\"", ctx.SubID)
|
||||||
|
headerValue := ctx.Request.Header.Get(model.AuthHeaderGateway)
|
||||||
|
if len(headerValue) != 0 {
|
||||||
|
if err := s.validate(ctx, headerValue); err != nil {
|
||||||
|
ctx.RespHeader.Set(model.UnaAuthorizedHeaderGateway, unauthHeader)
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("failed to validate %s: %w", model.AuthHeaderGateway, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerValue = ctx.Request.Header.Get(model.AuthHeaderSubscriber)
|
||||||
|
if len(headerValue) == 0 {
|
||||||
|
ctx.RespHeader.Set(model.UnaAuthorizedHeaderSubscriber, unauthHeader)
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("%s missing", model.UnaAuthorizedHeaderSubscriber))
|
||||||
|
}
|
||||||
|
if err := s.validate(ctx, headerValue); err != nil {
|
||||||
|
ctx.RespHeader.Set(model.UnaAuthorizedHeaderSubscriber, unauthHeader)
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("failed to validate %s: %w", model.AuthHeaderSubscriber, err))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate checks the validity of the provided signature header.
|
||||||
|
func (s *validateSignStep) validate(ctx *model.StepContext, value string) error {
|
||||||
|
headerParts := strings.Split(value, "|")
|
||||||
|
ids := strings.Split(headerParts[0], "\"")
|
||||||
|
if len(ids) < 2 || len(headerParts) < 3 {
|
||||||
|
return fmt.Errorf("malformed sign header")
|
||||||
|
}
|
||||||
|
subID := ids[1]
|
||||||
|
keyID := headerParts[1]
|
||||||
|
key, err := s.km.SigningPublicKey(ctx, subID, keyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get validation key: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.validator.Validate(ctx, ctx.Body, value, key); err != nil {
|
||||||
|
return fmt.Errorf("sign validation failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateSchemaStep represents the schema validation step.
|
||||||
|
type validateSchemaStep struct {
|
||||||
|
validator definition.SchemaValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
// newValidateSchemaStep creates and returns the validateSchema step after validation.
|
||||||
|
func newValidateSchemaStep(schemaValidator definition.SchemaValidator) (definition.Step, error) {
|
||||||
|
if schemaValidator == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: SchemaValidator plugin not configured")
|
||||||
|
}
|
||||||
|
log.Debug(context.Background(), "adding schema validator")
|
||||||
|
return &validateSchemaStep{validator: schemaValidator}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the schema validation step.
|
||||||
|
func (s *validateSchemaStep) Run(ctx *model.StepContext) error {
|
||||||
|
if err := s.validator.Validate(ctx, ctx.Request.URL, ctx.Body); err != nil {
|
||||||
|
return fmt.Errorf("schema validation failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRouteStep represents the route determination step.
|
||||||
|
type addRouteStep struct {
|
||||||
|
router definition.Router
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAddRouteStep creates and returns the addRoute step after validation.
|
||||||
|
func newAddRouteStep(router definition.Router) (definition.Step, error) {
|
||||||
|
if router == nil {
|
||||||
|
return nil, fmt.Errorf("invalid config: Router plugin not configured")
|
||||||
|
}
|
||||||
|
return &addRouteStep{router: router}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes the routing step.
|
||||||
|
func (s *addRouteStep) Run(ctx *model.StepContext) error {
|
||||||
|
route, err := s.router.Route(ctx, ctx.Request.URL, ctx.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to determine route: %w", err)
|
||||||
|
}
|
||||||
|
ctx.Route = &model.Route{
|
||||||
|
TargetType: route.TargetType,
|
||||||
|
PublisherID: route.PublisherID,
|
||||||
|
URL: route.URL,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
core/module/module.go
Normal file
82
core/module/module.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package module
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/core/module/handler"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the configuration for a module.
|
||||||
|
type Config struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Handler handler.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider represents a function that initializes an HTTP handler using a PluginManager.
|
||||||
|
type Provider func(ctx context.Context, mgr handler.PluginManager, cfg *handler.Config) (http.Handler, error)
|
||||||
|
|
||||||
|
// handlerProviders maintains a mapping of handler types to their respective providers.
|
||||||
|
var handlerProviders = map[handler.Type]Provider{
|
||||||
|
handler.HandlerTypeStd: handler.NewStdHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register initializes and registers handlers based on the provided configuration.
|
||||||
|
// It iterates over the module configurations, retrieves appropriate handler providers,
|
||||||
|
// and registers the handlers with the HTTP multiplexer.
|
||||||
|
func Register(ctx context.Context, mCfgs []Config, mux *http.ServeMux, mgr handler.PluginManager) error {
|
||||||
|
log.Debugf(ctx, "Registering modules with config: %#v", mCfgs)
|
||||||
|
// Iterate over the handlers in the configuration.
|
||||||
|
for _, c := range mCfgs {
|
||||||
|
rmp, ok := handlerProviders[c.Handler.Type]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid module : %s", c.Name)
|
||||||
|
}
|
||||||
|
h, err := rmp(ctx, mgr, &c.Handler)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s : %w", c.Name, err)
|
||||||
|
}
|
||||||
|
h, err = addMiddleware(ctx, mgr, h, &c.Handler)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add middleware: %w", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
h = moduleCtxMiddleware(c.Name, h)
|
||||||
|
log.Debugf(ctx, "Registering handler %s, of type %s @ %s", c.Name, c.Handler.Type, c.Path)
|
||||||
|
mux.Handle(c.Path, h)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMiddleware applies middleware plugins to the provided handler in reverse order.
|
||||||
|
// It retrieves middleware instances from the plugin manager and chains them to the handler.
|
||||||
|
func addMiddleware(ctx context.Context, mgr handler.PluginManager, handler http.Handler, hCfg *handler.Config) (http.Handler, error) {
|
||||||
|
mws := hCfg.Plugins.Middleware
|
||||||
|
log.Debugf(ctx, "Applying %d middleware(s) to the handler", len(mws))
|
||||||
|
// Apply the middleware in reverse order.
|
||||||
|
for i := len(mws) - 1; i >= 0; i-- {
|
||||||
|
log.Debugf(ctx, "Loading middleware: %s", mws[i].ID)
|
||||||
|
mw, err := mgr.Middleware(ctx, &mws[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(ctx, err, "Failed to load middleware %s: %v", mws[i].ID, err)
|
||||||
|
return nil, fmt.Errorf("failed to load middleware %s: %w", mws[i].ID, err)
|
||||||
|
}
|
||||||
|
// Apply the middleware to the handler.
|
||||||
|
handler = mw(handler)
|
||||||
|
log.Debugf(ctx, "Applied middleware: %s", mws[i].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf(ctx, "Middleware chain setup completed")
|
||||||
|
return handler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func moduleCtxMiddleware(moduleName string, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := context.WithValue(r.Context(), model.ContextKeyModuleID, moduleName)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
175
core/module/module_test.go
Normal file
175
core/module/module_test.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package module
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/core/module/handler"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockPluginManager is a mock implementation of the PluginManager interface
|
||||||
|
// with support for dynamically setting behavior.
|
||||||
|
type mockPluginManager struct {
|
||||||
|
middlewareFunc func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns a mock middleware function based on the provided configuration.
|
||||||
|
func (m *mockPluginManager) Middleware(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
return m.middlewareFunc(ctx, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignValidator returns a mock verifier implementation.
|
||||||
|
func (m *mockPluginManager) SignValidator(ctx context.Context, cfg *plugin.Config) (definition.SignValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator returns a mock schema validator implementation.
|
||||||
|
func (m *mockPluginManager) Validator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router returns a mock router implementation.
|
||||||
|
func (m *mockPluginManager) Router(ctx context.Context, cfg *plugin.Config) (definition.Router, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publisher returns a mock publisher implementation.
|
||||||
|
func (m *mockPluginManager) Publisher(ctx context.Context, cfg *plugin.Config) (definition.Publisher, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer returns a mock signer implementation.
|
||||||
|
func (m *mockPluginManager) Signer(ctx context.Context, cfg *plugin.Config) (definition.Signer, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step returns a mock step implementation.
|
||||||
|
func (m *mockPluginManager) Step(ctx context.Context, cfg *plugin.Config) (definition.Step, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache returns a mock cache implementation.
|
||||||
|
func (m *mockPluginManager) Cache(ctx context.Context, cfg *plugin.Config) (definition.Cache, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyManager returns a mock key manager implementation.
|
||||||
|
func (m *mockPluginManager) KeyManager(ctx context.Context, cache definition.Cache, rLookup definition.RegistryLookup, cfg *plugin.Config) (definition.KeyManager, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaValidator returns a mock schema validator implementation.
|
||||||
|
func (m *mockPluginManager) SchemaValidator(ctx context.Context, cfg *plugin.Config) (definition.SchemaValidator, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterSuccess tests scenarios where the handler registration should succeed.
|
||||||
|
func TestRegisterSuccess(t *testing.T) {
|
||||||
|
mCfgs := []Config{
|
||||||
|
{
|
||||||
|
Name: "test-module",
|
||||||
|
Path: "/test",
|
||||||
|
Handler: handler.Config{
|
||||||
|
Type: handler.HandlerTypeStd,
|
||||||
|
Plugins: handler.PluginCfg{
|
||||||
|
Middleware: []plugin.Config{{ID: "mock-middleware"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockManager := &mockPluginManager{
|
||||||
|
middlewareFunc: func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
err := Register(context.Background(), mCfgs, mux, mockManager)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a request and a response recorder
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Create a handler that extracts context
|
||||||
|
var capturedModuleName any
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
capturedModuleName = r.Context().Value(model.ContextKeyModuleID)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
wrappedHandler := moduleCtxMiddleware("test-module", testHandler)
|
||||||
|
wrappedHandler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Now verify if module name exists in context
|
||||||
|
if capturedModuleName != "test-module" {
|
||||||
|
t.Errorf("expected module_id in context to be 'test-module', got %v", capturedModuleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegisterFailure tests scenarios where the handler registration should fail.
|
||||||
|
func TestRegisterFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mCfgs []Config
|
||||||
|
mockManager *mockPluginManager
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid handler type",
|
||||||
|
mCfgs: []Config{
|
||||||
|
{
|
||||||
|
Name: "invalid-module",
|
||||||
|
Path: "/invalid",
|
||||||
|
Handler: handler.Config{
|
||||||
|
Type: "invalid-type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockManager: &mockPluginManager{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "middleware error",
|
||||||
|
mCfgs: []Config{
|
||||||
|
{
|
||||||
|
Name: "test-module",
|
||||||
|
Path: "/test",
|
||||||
|
Handler: handler.Config{
|
||||||
|
Type: handler.HandlerTypeStd,
|
||||||
|
Plugins: handler.PluginCfg{
|
||||||
|
Middleware: []plugin.Config{{ID: "mock-middleware"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mockManager: &mockPluginManager{
|
||||||
|
middlewareFunc: func(ctx context.Context, cfg *plugin.Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
return nil, errors.New("middleware error")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
err := Register(context.Background(), tt.mCfgs, mux, tt.mockManager)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected an error but got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
34
go.mod
34
go.mod
@@ -1,10 +1,38 @@
|
|||||||
module github.com/beckn/beckn-onix
|
module github.com/beckn/beckn-onix
|
||||||
|
|
||||||
go 1.23.4
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
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
|
golang.org/x/crypto v0.36.0
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.14.0 // indirect
|
require github.com/stretchr/testify v1.10.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03
|
||||||
|
|
||||||
|
require golang.org/x/text v0.23.0 // indirect
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|||||||
60
go.sum
60
go.sum
@@ -1,10 +1,64 @@
|
|||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||||
|
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||||
|
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
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=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03 h1:m1h+vudopHsI67FPT9MOncyndWhTcdUoBtI1R1uajGY=
|
||||||
|
github.com/zenazn/pkcs7pad v0.0.0-20170308005700-253a5b1f0e03/go.mod h1:8sheVFH84v3PCyFY/O02mIgSQY9I6wMYPWsq7mDnEZY=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
295
pkg/log/log.go
Normal file
295
pkg/log/log.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type level string
|
||||||
|
|
||||||
|
type destinationType string
|
||||||
|
|
||||||
|
type destination struct {
|
||||||
|
Type destinationType `yaml:"type"`
|
||||||
|
Config map[string]string `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destination types for logging output.
|
||||||
|
const (
|
||||||
|
Stdout destinationType = "stdout"
|
||||||
|
File destinationType = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Log levels define the severity of log messages.
|
||||||
|
const (
|
||||||
|
DebugLevel level = "debug"
|
||||||
|
InfoLevel level = "info"
|
||||||
|
WarnLevel level = "warn"
|
||||||
|
ErrorLevel level = "error"
|
||||||
|
FatalLevel level = "fatal"
|
||||||
|
PanicLevel level = "panic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logLevels = map[level]zerolog.Level{
|
||||||
|
DebugLevel: zerolog.DebugLevel,
|
||||||
|
InfoLevel: zerolog.InfoLevel,
|
||||||
|
WarnLevel: zerolog.WarnLevel,
|
||||||
|
ErrorLevel: zerolog.ErrorLevel,
|
||||||
|
FatalLevel: zerolog.FatalLevel,
|
||||||
|
PanicLevel: zerolog.PanicLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents the configuration for logging.
|
||||||
|
type Config struct {
|
||||||
|
Level level `yaml:"level"`
|
||||||
|
Destinations []destination `yaml:"destinations"`
|
||||||
|
ContextKeys []model.ContextKey `yaml:"contextKeys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger zerolog.Logger
|
||||||
|
cfg Config
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger instance and configuration.
|
||||||
|
var (
|
||||||
|
ErrInvalidLogLevel = errors.New("invalid log level")
|
||||||
|
ErrLogDestinationNil = errors.New("log Destinations cant be empty")
|
||||||
|
ErrMissingFilePath = errors.New("file path missing in destination config for file logging")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (config *Config) validate() error {
|
||||||
|
if _, exists := logLevels[config.Level]; !exists {
|
||||||
|
return ErrInvalidLogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Destinations) == 0 {
|
||||||
|
return ErrLogDestinationNil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dest := range config.Destinations {
|
||||||
|
switch dest.Type {
|
||||||
|
case Stdout:
|
||||||
|
case File:
|
||||||
|
if _, exists := dest.Config["path"]; !exists {
|
||||||
|
return ErrMissingFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range []string{"maxSize", "maxBackups", "maxAge"} {
|
||||||
|
if valStr, ok := dest.Config[key]; ok {
|
||||||
|
if _, err := strconv.Atoi(valStr); err != nil {
|
||||||
|
return fmt.Errorf("invalid %s: %w", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid destination type '%s'", dest.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultConfig = Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{Type: Stdout},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger, _ = getLogger(defaultConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogger(config Config) (zerolog.Logger, error) {
|
||||||
|
var newLogger zerolog.Logger
|
||||||
|
var writers []io.Writer
|
||||||
|
for _, dest := range config.Destinations {
|
||||||
|
switch dest.Type {
|
||||||
|
case Stdout:
|
||||||
|
writers = append(writers, os.Stdout)
|
||||||
|
case File:
|
||||||
|
filePath := dest.Config["path"]
|
||||||
|
dir := filepath.Dir(filePath)
|
||||||
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return newLogger, fmt.Errorf("failed to create log directory: %v", err)
|
||||||
|
}
|
||||||
|
lumberjackLogger := &lumberjack.Logger{
|
||||||
|
Filename: filePath,
|
||||||
|
Compress: false,
|
||||||
|
}
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return newLogger, fmt.Errorf("failed to get absolute path: %v", err)
|
||||||
|
}
|
||||||
|
lumberjackLogger.Filename = absPath
|
||||||
|
|
||||||
|
setConfigValue := func(key string, target *int) {
|
||||||
|
if valStr, ok := dest.Config[key]; ok {
|
||||||
|
if val, err := strconv.Atoi(valStr); err == nil {
|
||||||
|
*target = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConfigValue("maxSize", &lumberjackLogger.MaxSize)
|
||||||
|
setConfigValue("maxBackups", &lumberjackLogger.MaxBackups)
|
||||||
|
setConfigValue("maxAge", &lumberjackLogger.MaxAge)
|
||||||
|
if compress, ok := dest.Config["compress"]; ok {
|
||||||
|
lumberjackLogger.Compress = compress == "true"
|
||||||
|
}
|
||||||
|
writers = append(writers, lumberjackLogger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
multiwriter := io.MultiWriter(writers...)
|
||||||
|
defer func() {
|
||||||
|
if closer, ok := multiwriter.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
newLogger = zerolog.New(multiwriter).
|
||||||
|
Level(logLevels[config.Level]).
|
||||||
|
With().
|
||||||
|
Timestamp().
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
cfg = config
|
||||||
|
return newLogger, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitLogger initializes the logger with the given configuration.
|
||||||
|
// It ensures that the logger is initialized only once using sync.Once.
|
||||||
|
func InitLogger(c Config) error {
|
||||||
|
var initErr error
|
||||||
|
once.Do(func() {
|
||||||
|
if initErr = c.validate(); initErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, initErr = getLogger(c)
|
||||||
|
})
|
||||||
|
return initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a debug-level message with the provided context.
|
||||||
|
func Debug(ctx context.Context, msg string) {
|
||||||
|
logEvent(ctx, zerolog.DebugLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs a formatted debug-level message with the provided context.
|
||||||
|
func Debugf(ctx context.Context, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.DebugLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs an info-level message with the provided context.
|
||||||
|
func Info(ctx context.Context, msg string) {
|
||||||
|
logEvent(ctx, zerolog.InfoLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs a formatted info-level message with the provided context.
|
||||||
|
func Infof(ctx context.Context, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.InfoLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a warning-level message with the provided context.
|
||||||
|
func Warn(ctx context.Context, msg string) {
|
||||||
|
logEvent(ctx, zerolog.WarnLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs a formatted warning-level message with the provided context.
|
||||||
|
func Warnf(ctx context.Context, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.WarnLevel, msg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error-level message along with an error object.
|
||||||
|
func Error(ctx context.Context, err error, msg string) {
|
||||||
|
logEvent(ctx, zerolog.ErrorLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs a formatted error-level message along with an error object.
|
||||||
|
func Errorf(ctx context.Context, err error, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.ErrorLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a fatal-level message along with an error object and exits the application.
|
||||||
|
func Fatal(ctx context.Context, err error, msg string) {
|
||||||
|
logEvent(ctx, zerolog.FatalLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs a formatted fatal-level message along with an error object and exits the application.
|
||||||
|
func Fatalf(ctx context.Context, err error, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.FatalLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panic logs a panic-level message along with an error object and panics.
|
||||||
|
func Panic(ctx context.Context, err error, msg string) {
|
||||||
|
logEvent(ctx, zerolog.PanicLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panicf logs a formatted panic-level message along with an error object and panics.
|
||||||
|
func Panicf(ctx context.Context, err error, format string, v ...any) {
|
||||||
|
msg := fmt.Sprintf(format, v...)
|
||||||
|
logEvent(ctx, zerolog.PanicLevel, msg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// logEvent logs an event at the specified log level with an optional error message.
|
||||||
|
// It adds contextual information before logging the message.
|
||||||
|
func logEvent(ctx context.Context, level zerolog.Level, msg string, err error) {
|
||||||
|
event := logger.WithLevel(level)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
event = event.Err(err)
|
||||||
|
}
|
||||||
|
addCtx(ctx, event)
|
||||||
|
event.Msg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request logs details of an incoming HTTP request, including method, URL, body, and remote address.
|
||||||
|
func Request(ctx context.Context, r *http.Request, body []byte) {
|
||||||
|
event := logger.Info()
|
||||||
|
addCtx(ctx, event)
|
||||||
|
event.Str("method", r.Method).
|
||||||
|
Str("url", r.URL.String()).
|
||||||
|
Str("body", string(body)).
|
||||||
|
Str("remoteAddr", r.RemoteAddr).
|
||||||
|
Msg("HTTP Request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// addCtx adds context values to the log event based on configured context keys.
|
||||||
|
func addCtx(ctx context.Context, event *zerolog.Event) {
|
||||||
|
for _, key := range cfg.ContextKeys {
|
||||||
|
val, ok := ctx.Value(key).(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keyStr := string(key)
|
||||||
|
event.Any(keyStr, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response logs details of an outgoing HTTP response, including method, URL, status code, and response time.
|
||||||
|
func Response(ctx context.Context, r *http.Request, statusCode int, responseTime time.Duration) {
|
||||||
|
event := logger.Info()
|
||||||
|
addCtx(ctx, event)
|
||||||
|
event.Str("method", r.Method).
|
||||||
|
Str("url", r.URL.String()).
|
||||||
|
Int("statusCode", statusCode).
|
||||||
|
Dur("responseTime", responseTime).
|
||||||
|
Msg("HTTP Response")
|
||||||
|
}
|
||||||
673
pkg/log/log_test.go
Normal file
673
pkg/log/log_test.go
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey any
|
||||||
|
|
||||||
|
var requestID ctxKey = "requestID"
|
||||||
|
|
||||||
|
const testLogFilePath = "./test_logs/test.log"
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
// Create a single temporary directory for all tests
|
||||||
|
var err error
|
||||||
|
dir := filepath.Dir(testLogFilePath)
|
||||||
|
err = os.MkdirAll(dir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to create test log directory: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all tests
|
||||||
|
code := m.Run()
|
||||||
|
|
||||||
|
// Cleanup: Remove the log directory after all tests finish
|
||||||
|
err = os.RemoveAll(dir)
|
||||||
|
if err != nil {
|
||||||
|
println("failed to clean up test log directory: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit with the appropriate exit code
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLogger(t *testing.T, l level) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create a temporary directory for logs.
|
||||||
|
|
||||||
|
config := Config{
|
||||||
|
Level: l,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"path": testLogFilePath,
|
||||||
|
"maxSize": "1",
|
||||||
|
"maxAge": "1",
|
||||||
|
"maxBackup": "1",
|
||||||
|
"compress": "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ContextKeys: []model.ContextKey{
|
||||||
|
model.ContextKeyTxnID,
|
||||||
|
model.ContextKeyMsgID,
|
||||||
|
model.ContextKeySubscriberID,
|
||||||
|
model.ContextKeyModuleID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger with the given config
|
||||||
|
err := InitLogger(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to initialize logger: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return testLogFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLogFile(t *testing.T, logPath string) []string {
|
||||||
|
t.Helper()
|
||||||
|
b, err := os.ReadFile(logPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read log file: %v", err)
|
||||||
|
}
|
||||||
|
return strings.Split(string(b), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLogLine(t *testing.T, line string) map[string]interface{} {
|
||||||
|
t.Helper()
|
||||||
|
var logEntry map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(line), &logEntry)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse log line: %v", err)
|
||||||
|
}
|
||||||
|
return logEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDebug(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
logPath := setupLogger(t, DebugLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Debug(ctx, "Debug message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "debug",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Debug message",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected Debug message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfo(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, InfoLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Info(ctx, "Info message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "info",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Info message",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Info message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarn(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, WarnLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Warn(ctx, "Warning message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "warn",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Warning message",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
delete(logEntry, "time")
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Warning message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, ErrorLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Error(ctx, fmt.Errorf("test error"), "Error message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "error",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Error message",
|
||||||
|
"error": "test error",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Error message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequest(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, InfoLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), requestID, "abc-123")
|
||||||
|
req, _ := http.NewRequest("POST", "/api/test", bytes.NewBuffer([]byte(`{"key":"value"}`)))
|
||||||
|
req.RemoteAddr = "127.0.0.1:8080"
|
||||||
|
Request(ctx, req, []byte(`{"key":"value"}`))
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
if logEntry["message"] == "HTTP Request" || logEntry["method"] == "POST" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected formatted debug message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponse(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, InfoLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), requestID, "abc-123")
|
||||||
|
req, _ := http.NewRequest("GET", "/api/test", nil)
|
||||||
|
Response(ctx, req, 200, time.Millisecond*123)
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
if logEntry["message"] == "HTTP Response" {
|
||||||
|
if logEntry["message"] == "HTTP Response" {
|
||||||
|
value, ok := logEntry["statusCode"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected key 'statusCode' not found in log entry")
|
||||||
|
}
|
||||||
|
statusCode, ok := value.(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Value for 'statusCode' is not a float64, found: %T", value)
|
||||||
|
}
|
||||||
|
if statusCode == 200 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFatal(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, FatalLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Fatal(ctx, fmt.Errorf("fatal error"), "Fatal message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "fatal",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Fatal message",
|
||||||
|
"error": "fatal error",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Fatal message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPanic(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, PanicLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Panic(ctx, fmt.Errorf("panic error"), "Panic message")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "panic",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Panic message",
|
||||||
|
"error": "panic error",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Panic message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDebugf(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, DebugLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Debugf(ctx, "Debugf message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "debug",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Debugf message: test",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
t.Log(line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected formatted debug message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfof(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, InfoLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Infof(ctx, "Infof message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "info",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Infof message: test",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Infof message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWarnf(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, WarnLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
Warnf(ctx, "Warnf message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "warn",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Warnf message: test",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Warnf message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorf(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, ErrorLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
err := fmt.Errorf("error message")
|
||||||
|
Errorf(ctx, err, "Errorf message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "error",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Errorf message: test",
|
||||||
|
"error": "error message",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Errorf message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFatalf(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, FatalLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
err := fmt.Errorf("fatal error")
|
||||||
|
Fatalf(ctx, err, "Fatalf message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "fatal",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Fatalf message: test",
|
||||||
|
"error": "fatal error",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Fatalf message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPanicf(t *testing.T) {
|
||||||
|
logPath := setupLogger(t, PanicLevel)
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeySubscriberID, "12345")
|
||||||
|
err := fmt.Errorf("panic error")
|
||||||
|
Panicf(ctx, err, "Panicf message: %s", "test")
|
||||||
|
lines := readLogFile(t, logPath)
|
||||||
|
|
||||||
|
if len(lines) == 0 {
|
||||||
|
t.Fatal("No logs were written.")
|
||||||
|
}
|
||||||
|
expected := map[string]interface{}{
|
||||||
|
"level": "panic",
|
||||||
|
"subscriber_id": "12345",
|
||||||
|
"message": "Panicf message: test",
|
||||||
|
"error": "panic error",
|
||||||
|
}
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, line := range lines {
|
||||||
|
logEntry := parseLogLine(t, line)
|
||||||
|
// Ignore 'time' while comparing
|
||||||
|
delete(logEntry, "time")
|
||||||
|
|
||||||
|
if reflect.DeepEqual(expected, logEntry) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected Panicf message, but it was not found in logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid config with Stdout",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{Type: Stdout},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid config with File destination and valid path",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"path": "./logs/app.log",
|
||||||
|
"maxSize": "10",
|
||||||
|
"maxBackups": "5",
|
||||||
|
"maxAge": "7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Invalid log level",
|
||||||
|
config: Config{
|
||||||
|
Level: "invalid",
|
||||||
|
Destinations: []destination{
|
||||||
|
{Type: Stdout},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: ErrInvalidLogLevel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: No destinations provided",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{},
|
||||||
|
},
|
||||||
|
wantErr: ErrLogDestinationNil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Invalid destination type",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{Type: "unknown"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: fmt.Errorf("invalid destination type 'unknown'"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Missing file path for file destination",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"maxSize": "10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: ErrMissingFilePath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Invalid maxSize value in file destination",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"path": "./logs/app.log",
|
||||||
|
"maxSize": "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: errors.New(`invalid maxSize: strconv.Atoi: parsing "invalid": invalid syntax`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Invalid maxBackups value in file destination",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"path": "./logs/app.log",
|
||||||
|
"maxBackups": "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: errors.New(`invalid maxBackups: strconv.Atoi: parsing "invalid": invalid syntax`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error: Invalid maxAge value in file destination",
|
||||||
|
config: Config{
|
||||||
|
Level: InfoLevel,
|
||||||
|
Destinations: []destination{
|
||||||
|
{
|
||||||
|
Type: File,
|
||||||
|
Config: map[string]string{
|
||||||
|
"path": "./logs/app.log",
|
||||||
|
"maxAge": "invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: errors.New(`invalid maxAge: strconv.Atoi: parsing "invalid": invalid syntax`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := tt.config.validate()
|
||||||
|
if (err == nil) != (tt.wantErr == nil) {
|
||||||
|
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() {
|
||||||
|
t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
113
pkg/model/error.go
Normal file
113
pkg/model/error.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error represents a standard error response.
|
||||||
|
type Error struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Paths string `json:"paths,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implements the error interface for the Error struct.
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("Error: Code=%s, Path=%s, Message=%s", e.Code, e.Paths, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaValidationErr occurs when schema validation errors are encountered.
|
||||||
|
type SchemaValidationErr struct {
|
||||||
|
Errors []Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// This implements the error interface for SchemaValidationErr.
|
||||||
|
func (e *SchemaValidationErr) Error() string {
|
||||||
|
var errorMessages []string
|
||||||
|
for _, err := range e.Errors {
|
||||||
|
errorMessages = append(errorMessages, fmt.Sprintf("%s: %s", err.Paths, err.Message))
|
||||||
|
}
|
||||||
|
return strings.Join(errorMessages, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BecknError converts the SchemaValidationErr to an instance of Error.
|
||||||
|
func (e *SchemaValidationErr) BecknError() *Error {
|
||||||
|
if len(e.Errors) == 0 {
|
||||||
|
return &Error{
|
||||||
|
Code: http.StatusText(http.StatusBadRequest),
|
||||||
|
Message: "Schema validation error.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all error paths and messages
|
||||||
|
var paths []string
|
||||||
|
var messages []string
|
||||||
|
for _, err := range e.Errors {
|
||||||
|
if err.Paths != "" {
|
||||||
|
paths = append(paths, err.Paths)
|
||||||
|
}
|
||||||
|
messages = append(messages, err.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Error{
|
||||||
|
Code: http.StatusText(http.StatusBadRequest),
|
||||||
|
Paths: strings.Join(paths, ";"),
|
||||||
|
Message: strings.Join(messages, "; "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignValidationErr occurs when signature validation fails.
|
||||||
|
type SignValidationErr struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSignValidationErr creates a new instance of SignValidationErr from an error.
|
||||||
|
func NewSignValidationErr(e error) *SignValidationErr {
|
||||||
|
return &SignValidationErr{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BecknError converts the SignValidationErr to an instance of Error.
|
||||||
|
func (e *SignValidationErr) BecknError() *Error {
|
||||||
|
return &Error{
|
||||||
|
Code: http.StatusText(http.StatusUnauthorized),
|
||||||
|
Message: "Signature Validation Error: " + e.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadReqErr occurs when a bad request is encountered.
|
||||||
|
type BadReqErr struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBadReqErr creates a new instance of BadReqErr from an error.
|
||||||
|
func NewBadReqErr(err error) *BadReqErr {
|
||||||
|
return &BadReqErr{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BecknError converts the BadReqErr to an instance of Error.
|
||||||
|
func (e *BadReqErr) BecknError() *Error {
|
||||||
|
return &Error{
|
||||||
|
Code: http.StatusText(http.StatusBadRequest),
|
||||||
|
Message: "BAD Request: " + e.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFoundErr occurs when a requested endpoint is not found.
|
||||||
|
type NotFoundErr struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotFoundErr creates a new instance of NotFoundErr from an error.
|
||||||
|
func NewNotFoundErr(err error) *NotFoundErr {
|
||||||
|
return &NotFoundErr{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BecknError converts the NotFoundErr to an instance of Error.
|
||||||
|
func (e *NotFoundErr) BecknError() *Error {
|
||||||
|
return &Error{
|
||||||
|
Code: http.StatusText(http.StatusNotFound),
|
||||||
|
Message: "Endpoint not found: " + e.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
252
pkg/model/error_test.go
Normal file
252
pkg/model/error_test.go
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSignValidationErrf creates a new SignValidationErr with a formatted error message.
|
||||||
|
func NewSignValidationErrf(format string, a ...any) *SignValidationErr {
|
||||||
|
return &SignValidationErr{fmt.Errorf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotFoundErrf creates a new NotFoundErr with a formatted error message.
|
||||||
|
func NewNotFoundErrf(format string, a ...any) *NotFoundErr {
|
||||||
|
return &NotFoundErr{fmt.Errorf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBadReqErrf creates a new BadReqErr with a formatted error message.
|
||||||
|
func NewBadReqErrf(format string, a ...any) *BadReqErr {
|
||||||
|
return &BadReqErr{fmt.Errorf(format, a...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestError_Error(t *testing.T) {
|
||||||
|
err := &Error{
|
||||||
|
Code: "404",
|
||||||
|
Paths: "/api/v1/user",
|
||||||
|
Message: "User not found",
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "Error: Code=404, Path=/api/v1/user, Message=User not found"
|
||||||
|
actual := err.Error()
|
||||||
|
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidationErr_Error(t *testing.T) {
|
||||||
|
schemaErr := &SchemaValidationErr{
|
||||||
|
Errors: []Error{
|
||||||
|
{Paths: "/user", Message: "Field required"},
|
||||||
|
{Paths: "/email", Message: "Invalid format"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "/user: Field required; /email: Invalid format"
|
||||||
|
actual := schemaErr.Error()
|
||||||
|
|
||||||
|
if actual != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidationErr_BecknError(t *testing.T) {
|
||||||
|
schemaErr := &SchemaValidationErr{
|
||||||
|
Errors: []Error{
|
||||||
|
{Paths: "/user", Message: "Field required"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beErr := schemaErr.BecknError()
|
||||||
|
expected := "Bad Request"
|
||||||
|
if beErr.Code != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
beErr.Code, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignValidationErr_BecknError(t *testing.T) {
|
||||||
|
signErr := NewSignValidationErr(errors.New("signature failed"))
|
||||||
|
beErr := signErr.BecknError()
|
||||||
|
|
||||||
|
expectedMsg := "Signature Validation Error: signature failed"
|
||||||
|
if beErr.Message != expectedMsg {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
beErr.Message, expectedMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSignValidationErrf(t *testing.T) {
|
||||||
|
signErr := NewSignValidationErrf("error %s", "signature failed")
|
||||||
|
expected := "error signature failed"
|
||||||
|
if signErr.Error() != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
signErr.Error(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSignValidationErr(t *testing.T) {
|
||||||
|
err := errors.New("signature error")
|
||||||
|
signErr := NewSignValidationErr(err)
|
||||||
|
|
||||||
|
if signErr.Error() != err.Error() {
|
||||||
|
t.Errorf("err.Error() = %s, want %s", err.Error(),
|
||||||
|
signErr.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadReqErr_BecknError(t *testing.T) {
|
||||||
|
badReqErr := NewBadReqErr(errors.New("invalid input"))
|
||||||
|
beErr := badReqErr.BecknError()
|
||||||
|
|
||||||
|
expectedMsg := "BAD Request: invalid input"
|
||||||
|
if beErr.Message != expectedMsg {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
beErr.Message, expectedMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewBadReqErrf(t *testing.T) {
|
||||||
|
badReqErr := NewBadReqErrf("invalid field %s", "name")
|
||||||
|
expected := "invalid field name"
|
||||||
|
if badReqErr.Error() != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
badReqErr, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewBadReqErr(t *testing.T) {
|
||||||
|
err := errors.New("bad request")
|
||||||
|
badReqErr := NewBadReqErr(err)
|
||||||
|
|
||||||
|
if badReqErr.Error() != err.Error() {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
badReqErr.Error(), err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNotFoundErr_BecknError(t *testing.T) {
|
||||||
|
notFoundErr := NewNotFoundErr(errors.New("resource not found"))
|
||||||
|
beErr := notFoundErr.BecknError()
|
||||||
|
|
||||||
|
expectedMsg := "Endpoint not found: resource not found"
|
||||||
|
if beErr.Message != expectedMsg {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
beErr.Message, expectedMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNotFoundErrf(t *testing.T) {
|
||||||
|
notFoundErr := NewNotFoundErrf("resource %s not found", "user")
|
||||||
|
expected := "resource user not found"
|
||||||
|
if notFoundErr.Error() != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
notFoundErr.Error(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNotFoundErr(t *testing.T) {
|
||||||
|
err := errors.New("not found")
|
||||||
|
notFoundErr := NewNotFoundErr(err)
|
||||||
|
|
||||||
|
if notFoundErr.Error() != err.Error() {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
notFoundErr.Error(), err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRole_UnmarshalYAML_ValidRole(t *testing.T) {
|
||||||
|
var role Role
|
||||||
|
yamlData := []byte("bap")
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(yamlData, &role)
|
||||||
|
assert.NoError(t, err) //TODO: should replace assert here
|
||||||
|
assert.Equal(t, RoleBAP, role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRole_UnmarshalYAML_InvalidRole(t *testing.T) {
|
||||||
|
var role Role
|
||||||
|
yamlData := []byte("invalid")
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(yamlData, &role)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "invalid Role")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSchemaValidationErr_BecknError_NoErrors(t *testing.T) {
|
||||||
|
schemaValidationErr := &SchemaValidationErr{Errors: nil}
|
||||||
|
beErr := schemaValidationErr.BecknError()
|
||||||
|
|
||||||
|
expectedMsg := "Schema validation error."
|
||||||
|
expectedCode := http.StatusText(http.StatusBadRequest)
|
||||||
|
|
||||||
|
if beErr.Message != expectedMsg {
|
||||||
|
t.Errorf("beErr.Message = %s, want %s", beErr.Message, expectedMsg)
|
||||||
|
}
|
||||||
|
if beErr.Code != expectedCode {
|
||||||
|
t.Errorf("beErr.Code = %s, want %s", beErr.Code, expectedCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseContextKey_ValidKeys(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected ContextKey
|
||||||
|
}{
|
||||||
|
{"transaction_id", ContextKeyTxnID},
|
||||||
|
{"message_id", ContextKeyMsgID},
|
||||||
|
{"subscriber_id", ContextKeySubscriberID},
|
||||||
|
{"module_id", ContextKeyModuleID},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
key, err := ParseContextKey(tt.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error for input %s: %v", tt.input, err)
|
||||||
|
}
|
||||||
|
if key != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseContextKey_InvalidKey(t *testing.T) {
|
||||||
|
_, err := ParseContextKey("invalid_key")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid context key, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextKey_UnmarshalYAML_Valid(t *testing.T) {
|
||||||
|
yamlData := []byte("message_id")
|
||||||
|
var key ContextKey
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(yamlData, &key)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if key != ContextKeyMsgID {
|
||||||
|
t.Errorf("expected %s, got %s", ContextKeyMsgID, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextKey_UnmarshalYAML_Invalid(t *testing.T) {
|
||||||
|
yamlData := []byte("invalid_key")
|
||||||
|
var key ContextKey
|
||||||
|
|
||||||
|
err := yaml.Unmarshal(yamlData, &key)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid context key, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
186
pkg/model/model.go
Normal file
186
pkg/model/model.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscriber represents a unique operational configuration of a trusted platform on a network.
|
||||||
|
type Subscriber struct {
|
||||||
|
SubscriberID string `json:"subscriber_id"`
|
||||||
|
URL string `json:"url" format:"uri"`
|
||||||
|
Type string `json:"type" enum:"BAP,BPP,BG"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscription represents subscription details of a network participant.
|
||||||
|
type Subscription struct {
|
||||||
|
Subscriber `json:",inline"`
|
||||||
|
KeyID string `json:"key_id" format:"uuid"`
|
||||||
|
SigningPublicKey string `json:"signing_public_key"`
|
||||||
|
EncrPublicKey string `json:"encr_public_key"`
|
||||||
|
ValidFrom time.Time `json:"valid_from" format:"date-time"`
|
||||||
|
ValidUntil time.Time `json:"valid_until" format:"date-time"`
|
||||||
|
Status string `json:"status" enum:"INITIATED,UNDER_SUBSCRIPTION,SUBSCRIBED,EXPIRED,UNSUBSCRIBED,INVALID_SSL"`
|
||||||
|
Created time.Time `json:"created" format:"date-time"`
|
||||||
|
Updated time.Time `json:"updated" format:"date-time"`
|
||||||
|
Nonce string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization-related constants for headers.
|
||||||
|
const (
|
||||||
|
AuthHeaderSubscriber string = "Authorization"
|
||||||
|
AuthHeaderGateway string = "X-Gateway-Authorization"
|
||||||
|
UnaAuthorizedHeaderSubscriber string = "WWW-Authenticate"
|
||||||
|
UnaAuthorizedHeaderGateway string = "Proxy-Authenticate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContextKey is a custom type used as a key for storing and retrieving values in a context.
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ContextKeyTxnID is the context key used to store and retrieve the transaction ID in a request context.
|
||||||
|
ContextKeyTxnID ContextKey = "transaction_id"
|
||||||
|
|
||||||
|
// ContextKeyMsgID is the context key used to store and retrieve the message ID in a request context.
|
||||||
|
ContextKeyMsgID ContextKey = "message_id"
|
||||||
|
|
||||||
|
// ContextKeySubscriberID is the context key used to store and retrieve the subscriber ID in a request context.
|
||||||
|
ContextKeySubscriberID ContextKey = "subscriber_id"
|
||||||
|
|
||||||
|
// ContextKeyModuleID is the context key for storing and retrieving the model ID from a request context.
|
||||||
|
ContextKeyModuleID ContextKey = "module_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
var contextKeys = map[string]ContextKey{
|
||||||
|
"transaction_id": ContextKeyTxnID,
|
||||||
|
"message_id": ContextKeyMsgID,
|
||||||
|
"subscriber_id": ContextKeySubscriberID,
|
||||||
|
"module_id": ContextKeyModuleID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseContextKey converts a string into a valid ContextKey.
|
||||||
|
func ParseContextKey(v string) (ContextKey, error) {
|
||||||
|
key, ok := contextKeys[v]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("invalid context key: %s", v)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML ensures that only known context keys are accepted during YAML unmarshalling.
|
||||||
|
func (k *ContextKey) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var keyStr string
|
||||||
|
if err := unmarshal(&keyStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedKey, err := ParseContextKey(keyStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*k = parsedKey
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role defines the type of participant in the network.
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RoleBAP represents a Buyer App Participant (BAP) in the network.
|
||||||
|
RoleBAP Role = "bap"
|
||||||
|
// RoleBPP represents a Buyer Platform Participant (BPP) in the network.
|
||||||
|
RoleBPP Role = "bpp"
|
||||||
|
// RoleGateway represents a Gateway that facilitates communication in the network.
|
||||||
|
RoleGateway Role = "gateway"
|
||||||
|
// RoleRegistery represents the Registry that maintains network participant details.
|
||||||
|
RoleRegistery Role = "registery"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validRoles = map[Role]bool{
|
||||||
|
RoleBAP: true,
|
||||||
|
RoleBPP: true,
|
||||||
|
RoleGateway: true,
|
||||||
|
RoleRegistery: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements custom YAML unmarshalling for Role to ensure only valid values are accepted.
|
||||||
|
func (r *Role) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
var roleName string
|
||||||
|
if err := unmarshal(&roleName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
role := Role(roleName)
|
||||||
|
if !validRoles[role] {
|
||||||
|
return fmt.Errorf("invalid Role: %s", roleName)
|
||||||
|
}
|
||||||
|
*r = role
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route represents a network route for message processing.
|
||||||
|
type Route struct {
|
||||||
|
TargetType string // "url" or "publisher"
|
||||||
|
PublisherID string // For message queues
|
||||||
|
URL *url.URL // For API calls
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyset represents a collection of cryptographic keys used for signing and encryption.
|
||||||
|
type Keyset struct {
|
||||||
|
UniqueKeyID string // UniqueKeyID is the identifier for the key pair.
|
||||||
|
SigningPrivate string // SigningPrivate is the private key used for signing operations.
|
||||||
|
SigningPublic string // SigningPublic is the public key corresponding to the signing private key.
|
||||||
|
EncrPrivate string // EncrPrivate is the private key used for encryption operations.
|
||||||
|
EncrPublic string // EncrPublic is the public key corresponding to the encryption private key.
|
||||||
|
}
|
||||||
|
|
||||||
|
// StepContext holds context information for a request processing step.
|
||||||
|
type StepContext struct {
|
||||||
|
context.Context
|
||||||
|
Request *http.Request
|
||||||
|
Body []byte
|
||||||
|
Route *Route
|
||||||
|
SubID string
|
||||||
|
Role Role
|
||||||
|
RespHeader http.Header
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext updates the existing StepContext with a new context.
|
||||||
|
func (ctx *StepContext) WithContext(newCtx context.Context) {
|
||||||
|
ctx.Context = newCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status represents the acknowledgment status in a response.
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// StatusACK indicates a successful acknowledgment.
|
||||||
|
StatusACK Status = "ACK"
|
||||||
|
// StatusNACK indicates a negative acknowledgment or failure.
|
||||||
|
StatusNACK Status = "NACK"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ack represents an acknowledgment response.
|
||||||
|
type Ack struct {
|
||||||
|
// Status holds the acknowledgment status (ACK/NACK).
|
||||||
|
Status Status `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents the structure of a response message.
|
||||||
|
type Message struct {
|
||||||
|
// Ack contains the acknowledgment status.
|
||||||
|
Ack Ack `json:"ack"`
|
||||||
|
// Error holds error details, if any, in the response.
|
||||||
|
Error *Error `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response represents the main response structure.
|
||||||
|
type Response struct {
|
||||||
|
Message Message `json:"message"`
|
||||||
|
}
|
||||||
21
pkg/plugin/config.go
Normal file
21
pkg/plugin/config.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
type PublisherCfg struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Config map[string]string `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidatorCfg struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Config map[string]string `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Config map[string]string `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManagerConfig struct {
|
||||||
|
Root string `yaml:"root"`
|
||||||
|
RemoteRoot string `yaml:"remoteRoot"`
|
||||||
|
}
|
||||||
27
pkg/plugin/definition/cache.go
Normal file
27
pkg/plugin/definition/cache.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache defines the general cache interface for caching plugins.
|
||||||
|
type Cache interface {
|
||||||
|
// Get retrieves a value from the cache based on the given key.
|
||||||
|
Get(ctx context.Context, key string) (string, error)
|
||||||
|
|
||||||
|
// Set stores a value in the cache with the given key and TTL (time-to-live) in seconds.
|
||||||
|
Set(ctx context.Context, key, value string, ttl time.Duration) error
|
||||||
|
|
||||||
|
// Delete removes a value from the cache based on the given key.
|
||||||
|
Delete(ctx context.Context, key string) error
|
||||||
|
|
||||||
|
// Clear removes all values from the cache.
|
||||||
|
Clear(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheProvider interface defines the contract for managing cache instances.
|
||||||
|
type CacheProvider interface {
|
||||||
|
// New initializes a new cache instance with the given configuration.
|
||||||
|
New(ctx context.Context, config map[string]string) (Cache, func() error, error)
|
||||||
|
}
|
||||||
15
pkg/plugin/definition/decrypter.go
Normal file
15
pkg/plugin/definition/decrypter.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Decrypter defines the methods for decryption.
|
||||||
|
type Decrypter interface {
|
||||||
|
// Decrypt decrypts the given body using the provided privateKeyBase64 and publicKeyBase64.
|
||||||
|
Decrypt(ctx context.Context, encryptedData string, privateKeyBase64, publicKeyBase64 string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecrypterProvider initializes a new decrypter instance with the given config.
|
||||||
|
type DecrypterProvider interface {
|
||||||
|
// New creates a new decrypter instance based on the provided config.
|
||||||
|
New(ctx context.Context, config map[string]string) (Decrypter, func() error, error)
|
||||||
|
}
|
||||||
15
pkg/plugin/definition/encrypter.go
Normal file
15
pkg/plugin/definition/encrypter.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Encrypter defines the methods for encryption.
|
||||||
|
type Encrypter interface {
|
||||||
|
// Encrypt encrypts the given body using the provided privateKeyBase64 and publicKeyBase64.
|
||||||
|
Encrypt(ctx context.Context, data string, privateKeyBase64, publicKeyBase64 string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncrypterProvider initializes a new encrypter instance with the given config.
|
||||||
|
type EncrypterProvider interface {
|
||||||
|
// New creates a new encrypter instance based on the provided config.
|
||||||
|
New(ctx context.Context, config map[string]string) (Encrypter, func() error, error)
|
||||||
|
}
|
||||||
23
pkg/plugin/definition/keymanager.go
Normal file
23
pkg/plugin/definition/keymanager.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyManager defines the interface for key management operations/methods.
|
||||||
|
type KeyManager interface {
|
||||||
|
GenerateKeyPairs() (*model.Keyset, error)
|
||||||
|
StorePrivateKeys(ctx context.Context, keyID string, keys *model.Keyset) error
|
||||||
|
SigningPrivateKey(ctx context.Context, keyID string) (string, string, error)
|
||||||
|
EncrPrivateKey(ctx context.Context, keyID string) (string, string, error)
|
||||||
|
SigningPublicKey(ctx context.Context, subscriberID, uniqueKeyID string) (string, error)
|
||||||
|
EncrPublicKey(ctx context.Context, subscriberID, uniqueKeyID string) (string, error)
|
||||||
|
DeletePrivateKeys(ctx context.Context, keyID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyManagerProvider initializes a new signer instance.
|
||||||
|
type KeyManagerProvider interface {
|
||||||
|
New(context.Context, Cache, RegistryLookup, map[string]string) (KeyManager, func() error, error)
|
||||||
|
}
|
||||||
10
pkg/plugin/definition/middleware.go
Normal file
10
pkg/plugin/definition/middleware.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiddlewareProvider interface {
|
||||||
|
New(ctx context.Context, cfg map[string]string) (func(http.Handler) http.Handler, error)
|
||||||
|
}
|
||||||
14
pkg/plugin/definition/publisher.go
Normal file
14
pkg/plugin/definition/publisher.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Publisher defines the general publisher interface for messaging plugins.
|
||||||
|
type Publisher interface {
|
||||||
|
// Publish sends a message (as a byte slice) using the underlying messaging system.
|
||||||
|
Publish(context.Context, string, []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublisherProvider interface {
|
||||||
|
// New initializes a new publisher instance with the given configuration.
|
||||||
|
New(ctx context.Context, config map[string]string) (Publisher, func() error, error)
|
||||||
|
}
|
||||||
11
pkg/plugin/definition/registry.go
Normal file
11
pkg/plugin/definition/registry.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistryLookup interface {
|
||||||
|
Lookup(ctx context.Context, req *model.Subscription) ([]model.Subscription, error)
|
||||||
|
}
|
||||||
19
pkg/plugin/definition/router.go
Normal file
19
pkg/plugin/definition/router.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouterProvider initializes the a new Router instance with the given config.
|
||||||
|
type RouterProvider interface {
|
||||||
|
New(ctx context.Context, config map[string]string) (Router, func() error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router defines the interface for routing requests.
|
||||||
|
type Router interface {
|
||||||
|
// Route determines the routing destination based on the request context.
|
||||||
|
Route(ctx context.Context, url *url.URL, body []byte) (*model.Route, error)
|
||||||
|
}
|
||||||
16
pkg/plugin/definition/schemaValidator.go
Normal file
16
pkg/plugin/definition/schemaValidator.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchemaValidator interface for schema validation.
|
||||||
|
type SchemaValidator interface {
|
||||||
|
Validate(ctx context.Context, url *url.URL, payload []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaValidatorProvider interface for creating validators.
|
||||||
|
type SchemaValidatorProvider interface {
|
||||||
|
New(ctx context.Context, config map[string]string) (SchemaValidator, func() error, error)
|
||||||
|
}
|
||||||
17
pkg/plugin/definition/signer.go
Normal file
17
pkg/plugin/definition/signer.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
15
pkg/plugin/definition/signvalidator.go
Normal file
15
pkg/plugin/definition/signvalidator.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// SignValidator defines the method for verifying signatures.
|
||||||
|
type SignValidator interface {
|
||||||
|
// Validate checks the validity of the signature for the given body.
|
||||||
|
Validate(ctx context.Context, body []byte, header string, publicKeyBase64 string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignValidatorProvider initializes a new Verifier instance with the given config.
|
||||||
|
type SignValidatorProvider interface {
|
||||||
|
// New creates a new Verifier instance based on the provided config.
|
||||||
|
New(ctx context.Context, config map[string]string) (SignValidator, func() error, error)
|
||||||
|
}
|
||||||
15
pkg/plugin/definition/step.go
Normal file
15
pkg/plugin/definition/step.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package definition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Step interface {
|
||||||
|
Run(ctx *model.StepContext) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type StepProvider interface {
|
||||||
|
New(context.Context, map[string]string) (Step, func(), error)
|
||||||
|
}
|
||||||
19
pkg/plugin/implementation/decrypter/cmd/plugin.go
Normal file
19
pkg/plugin/implementation/decrypter/cmd/plugin.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
decrypter "github.com/beckn/beckn-onix/pkg/plugin/implementation/decrypter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decrypterProvider implements the definition.decrypterProvider interface.
|
||||||
|
type decrypterProvider struct{}
|
||||||
|
|
||||||
|
// New creates a new Decrypter instance using the provided configuration.
|
||||||
|
func (dp decrypterProvider) New(ctx context.Context, config map[string]string) (definition.Decrypter, func() error, error) {
|
||||||
|
return decrypter.New(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider = decrypterProvider{}
|
||||||
49
pkg/plugin/implementation/decrypter/cmd/plugin_test.go
Normal file
49
pkg/plugin/implementation/decrypter/cmd/plugin_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecrypterProviderSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid context with empty config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid context with non-empty config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"key": "value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
provider := decrypterProvider{}
|
||||||
|
decrypter, cleanup, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
// Check error.
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("New() error = %v, want no error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check decrypter.
|
||||||
|
if decrypter == nil {
|
||||||
|
t.Error("New() decrypter is nil, want non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cleanup function if it exists.
|
||||||
|
if cleanup != nil {
|
||||||
|
if err := cleanup(); err != nil {
|
||||||
|
t.Errorf("cleanup() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
87
pkg/plugin/implementation/decrypter/decrypter.go
Normal file
87
pkg/plugin/implementation/decrypter/decrypter.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package decryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zenazn/pkcs7pad"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decrypter implements the Decrypter interface and handles the decryption process.
|
||||||
|
type decrypter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new decrypter instance with the given configuration.
|
||||||
|
func New(ctx context.Context) (*decrypter, func() error, error) {
|
||||||
|
return &decrypter{}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts the given encryptedData using the provided privateKeyBase64 and publicKeyBase64.
|
||||||
|
func (d *decrypter) Decrypt(ctx context.Context, encryptedData, privateKeyBase64, publicKeyBase64 string) (string, error) {
|
||||||
|
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return "", model.NewBadReqErr(fmt.Errorf("invalid private key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return "", model.NewBadReqErr(fmt.Errorf("invalid public key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the Base64 encoded encrypted data.
|
||||||
|
messageByte, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return "", model.NewBadReqErr(fmt.Errorf("failed to decode encrypted data: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
aesCipher, err := createAESCipher(privateKeyBytes, publicKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create AES cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blocksize := aesCipher.BlockSize()
|
||||||
|
if len(messageByte)%blocksize != 0 {
|
||||||
|
return "", fmt.Errorf("ciphertext is not a multiple of the blocksize")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(messageByte); i += aesCipher.BlockSize() {
|
||||||
|
executionSlice := messageByte[i : i+aesCipher.BlockSize()]
|
||||||
|
aesCipher.Decrypt(executionSlice, executionSlice)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageByte, err = pkcs7pad.Unpad(messageByte)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to unpad data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(messageByte), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAESCipher(privateKey, publicKey []byte) (cipher.Block, error) {
|
||||||
|
x25519Curve := ecdh.X25519()
|
||||||
|
x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create private key: %w", err)
|
||||||
|
}
|
||||||
|
x25519PublicKey, err := x25519Curve.NewPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create public key: %w", err)
|
||||||
|
}
|
||||||
|
sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to derive shared secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aesCipher, err := aes.NewCipher(sharedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aesCipher, nil
|
||||||
|
}
|
||||||
251
pkg/plugin/implementation/decrypter/decrypter_test.go
Normal file
251
pkg/plugin/implementation/decrypter/decrypter_test.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package decryption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/zenazn/pkcs7pad"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to generate valid test keys.
|
||||||
|
func generateTestKeys(t *testing.T) (privateKeyB64, publicKeyB64 string) {
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
privateKey, err := curve.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := privateKey.PublicKey()
|
||||||
|
privateKeyB64 = base64.StdEncoding.EncodeToString(privateKey.Bytes())
|
||||||
|
publicKeyB64 = base64.StdEncoding.EncodeToString(publicKey.Bytes())
|
||||||
|
|
||||||
|
return privateKeyB64, publicKeyB64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to encrypt test data.
|
||||||
|
func encryptTestData(t *testing.T, data []byte, privateKeyBase64, publicKeyBase64 string) string {
|
||||||
|
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Invalid private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Invalid public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
x25519Curve := ecdh.X25519()
|
||||||
|
x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create private key: %v", err)
|
||||||
|
}
|
||||||
|
x25519PublicKey, err := x25519Curve.NewPublicKey(publicKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate shared secret for encryption.
|
||||||
|
sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create shared secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create AES cipher.
|
||||||
|
block, err := aes.NewCipher(sharedSecret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create AES cipher: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad the data.
|
||||||
|
paddedData := pkcs7pad.Pad(data, block.BlockSize())
|
||||||
|
|
||||||
|
// Encrypt the data.
|
||||||
|
ciphertext := make([]byte, len(paddedData))
|
||||||
|
for i := 0; i < len(paddedData); i += block.BlockSize() {
|
||||||
|
block.Encrypt(ciphertext[i:i+block.BlockSize()], paddedData[i:i+block.BlockSize()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDecrypterSuccess tests successful decryption scenarios.
|
||||||
|
func TestDecrypterSuccess(t *testing.T) {
|
||||||
|
senderPrivateKeyB64, senderPublicKeyB64 := generateTestKeys(t)
|
||||||
|
receiverPrivateKeyB64, receiverPublicKeyB64 := generateTestKeys(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid decryption with small data",
|
||||||
|
data: []byte("test"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid decryption with medium data",
|
||||||
|
data: []byte("medium length test data that spans multiple blocks"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid decryption with empty data",
|
||||||
|
data: []byte{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Encrypt the test data.
|
||||||
|
encryptedData := encryptTestData(t, tt.data, senderPrivateKeyB64, receiverPublicKeyB64)
|
||||||
|
|
||||||
|
decrypter, _, err := New(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create decrypter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := decrypter.Decrypt(context.Background(), encryptedData, receiverPrivateKeyB64, senderPublicKeyB64)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Decrypt() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if result != string(tt.data) {
|
||||||
|
t.Errorf("Decrypt() = %v, want %v", result, string(tt.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDecrypterFailure tests various failure scenarios.
|
||||||
|
func TestDecrypterFailure(t *testing.T) {
|
||||||
|
_, senderPublicKeyB64 := generateTestKeys(t)
|
||||||
|
receiverPrivateKeyB64, _ := generateTestKeys(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
encryptedData string
|
||||||
|
privateKey string
|
||||||
|
publicKey string
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid private key format",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: "invalid-base64!@#$",
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "invalid private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid public key format",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: "invalid-base64!@#$",
|
||||||
|
expectedErr: "invalid public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid encrypted data format",
|
||||||
|
encryptedData: "invalid-base64!@#$",
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "failed to decode encrypted data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty private key",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: "",
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "invalid private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty public key",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: "",
|
||||||
|
expectedErr: "invalid public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid base64 data",
|
||||||
|
encryptedData: "=invalid-base64", // Invalid encrypted data.
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "failed to decode encrypted data",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid private key size",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: base64.StdEncoding.EncodeToString([]byte("short")),
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "failed to create private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid public key size",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString(make([]byte, 32)),
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: base64.StdEncoding.EncodeToString([]byte("short")),
|
||||||
|
expectedErr: "failed to create public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid block size",
|
||||||
|
encryptedData: base64.StdEncoding.EncodeToString([]byte("not-block-size")),
|
||||||
|
privateKey: receiverPrivateKeyB64,
|
||||||
|
publicKey: senderPublicKeyB64,
|
||||||
|
expectedErr: "ciphertext is not a multiple of the blocksize",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
decrypter, _, err := New(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create decrypter: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = decrypter.Decrypt(context.Background(), tt.encryptedData, tt.privateKey, tt.publicKey)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), tt.expectedErr) {
|
||||||
|
t.Errorf("Expected error containing %q, got %q", tt.expectedErr, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewDecrypter tests the creation of new Decrypter instances.
|
||||||
|
func TestNewDecrypter(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid context",
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil context",
|
||||||
|
ctx: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
decrypter, _, err := New(tt.ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("New() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if decrypter == nil {
|
||||||
|
t.Error("Expected non-nil decrypter")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
18
pkg/plugin/implementation/encrypter/cmd/plugin.go
Normal file
18
pkg/plugin/implementation/encrypter/cmd/plugin.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/encrypter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encrypterProvider implements the definition.encrypterProvider interface.
|
||||||
|
type encrypterProvider struct{}
|
||||||
|
|
||||||
|
func (ep encrypterProvider) New(ctx context.Context, config map[string]string) (definition.Encrypter, func() error, error) {
|
||||||
|
return encrypter.New(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider = encrypterProvider{}
|
||||||
49
pkg/plugin/implementation/encrypter/cmd/plugin_test.go
Normal file
49
pkg/plugin/implementation/encrypter/cmd/plugin_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncrypterProviderSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid empty config",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid config with algorithm",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{
|
||||||
|
"algorithm": "AES",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create provider and encrypter.
|
||||||
|
provider := encrypterProvider{}
|
||||||
|
encrypter, cleanup, err := provider.New(tt.ctx, tt.config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EncrypterProvider.New() error = %v", err)
|
||||||
|
}
|
||||||
|
if encrypter == nil {
|
||||||
|
t.Fatal("EncrypterProvider.New() returned nil encrypter")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cleanup != nil {
|
||||||
|
if err := cleanup(); err != nil {
|
||||||
|
t.Errorf("Cleanup() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
71
pkg/plugin/implementation/encrypter/encrypter.go
Normal file
71
pkg/plugin/implementation/encrypter/encrypter.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package encrypter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"github.com/zenazn/pkcs7pad"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encrypter implements the Encrypter interface and handles the encryption process.
|
||||||
|
type encrypter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new encrypter instance with the given configuration.
|
||||||
|
func New(ctx context.Context) (*encrypter, func() error, error) {
|
||||||
|
return &encrypter{}, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *encrypter) Encrypt(ctx context.Context, data string, privateKeyBase64, publicKeyBase64 string) (string, error) {
|
||||||
|
privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return "", model.NewBadReqErr(fmt.Errorf("invalid private key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
return "", model.NewBadReqErr(fmt.Errorf("invalid public key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the input string to a byte slice.
|
||||||
|
dataByte := []byte(data)
|
||||||
|
aesCipher, err := createAESCipher(privateKeyBytes, publicKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create AES cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataByte = pkcs7pad.Pad(dataByte, aesCipher.BlockSize())
|
||||||
|
for i := 0; i < len(dataByte); i += aesCipher.BlockSize() {
|
||||||
|
aesCipher.Encrypt(dataByte[i:i+aesCipher.BlockSize()], dataByte[i:i+aesCipher.BlockSize()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(dataByte), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAESCipher(privateKey, publicKey []byte) (cipher.Block, error) {
|
||||||
|
x25519Curve := ecdh.X25519()
|
||||||
|
x25519PrivateKey, err := x25519Curve.NewPrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewBadReqErr(fmt.Errorf("failed to create private key: %w", err))
|
||||||
|
}
|
||||||
|
x25519PublicKey, err := x25519Curve.NewPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewBadReqErr(fmt.Errorf("failed to create public key: %w", err))
|
||||||
|
}
|
||||||
|
sharedSecret, err := x25519PrivateKey.ECDH(x25519PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to derive shared secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aesCipher, err := aes.NewCipher(sharedSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aesCipher, nil
|
||||||
|
}
|
||||||
183
pkg/plugin/implementation/encrypter/encrypter_test.go
Normal file
183
pkg/plugin/implementation/encrypter/encrypter_test.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package encrypter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to generate a test X25519 key pair.
|
||||||
|
func generateTestKeyPair(t *testing.T) (string, string) {
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
privateKey, err := curve.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to generate private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKeyBytes := privateKey.PublicKey().Bytes()
|
||||||
|
// Encode public and private key to base64.
|
||||||
|
publicKeyBase64 := base64.StdEncoding.EncodeToString(publicKeyBytes)
|
||||||
|
privateKeyBase64 := base64.StdEncoding.EncodeToString(privateKey.Bytes())
|
||||||
|
|
||||||
|
return publicKeyBase64, privateKeyBase64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncryptSuccess tests successful encryption scenarios.
|
||||||
|
func TestEncryptSuccess(t *testing.T) {
|
||||||
|
_, privateKey := generateTestKeyPair(t)
|
||||||
|
peerpublicKey, _ := generateTestKeyPair(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
pubKey string
|
||||||
|
privKey string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid short message",
|
||||||
|
data: "Hello, World!",
|
||||||
|
pubKey: peerpublicKey,
|
||||||
|
privKey: privateKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid JSON message",
|
||||||
|
data: `{"key":"value"}`,
|
||||||
|
pubKey: peerpublicKey,
|
||||||
|
privKey: privateKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid empty message",
|
||||||
|
data: "",
|
||||||
|
pubKey: peerpublicKey,
|
||||||
|
privKey: privateKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
encrypter := &encrypter{}
|
||||||
|
encrypted, err := encrypter.Encrypt(context.Background(), tt.data, tt.privKey, tt.pubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Encrypt() expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the encrypted data is valid base64.
|
||||||
|
_, err = base64.StdEncoding.DecodeString(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Encrypt() output is not valid base64: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we can't decrypt without the ephemeral private key,
|
||||||
|
// we can only verify that encryption doesn't return empty data.
|
||||||
|
if encrypted == "" {
|
||||||
|
t.Error("Encrypt() returned empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the output is different from input (basic encryption check).
|
||||||
|
if encrypted == tt.data {
|
||||||
|
t.Error("Encrypt() output matches input, suggesting no encryption occurred")
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncryptFailure tests encryption failure scenarios.
|
||||||
|
func TestEncryptFailure(t *testing.T) {
|
||||||
|
// Generate a valid key pair for testing.
|
||||||
|
_, privateKey := generateTestKeyPair(t)
|
||||||
|
peerpublicKey, _ := generateTestKeyPair(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
publicKey string
|
||||||
|
privKey string
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Invalid public key format",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: "invalid-base64!@#$",
|
||||||
|
privKey: privateKey,
|
||||||
|
errorContains: "invalid public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid key bytes(public key)",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: base64.StdEncoding.EncodeToString([]byte("invalid-key-bytes")),
|
||||||
|
privKey: privateKey,
|
||||||
|
errorContains: "failed to create public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid key bytes(private key)",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: peerpublicKey,
|
||||||
|
privKey: base64.StdEncoding.EncodeToString([]byte("invalid-key-bytes")),
|
||||||
|
errorContains: "failed to create private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty public key",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: "",
|
||||||
|
privKey: privateKey,
|
||||||
|
errorContains: "invalid public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Too short key",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: base64.StdEncoding.EncodeToString([]byte{1, 2, 3, 4}),
|
||||||
|
privKey: privateKey,
|
||||||
|
errorContains: "failed to create public key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid private key",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: peerpublicKey,
|
||||||
|
privKey: "invalid-base64!@#$",
|
||||||
|
errorContains: "invalid private key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty private key",
|
||||||
|
data: "test data",
|
||||||
|
publicKey: peerpublicKey,
|
||||||
|
privKey: "",
|
||||||
|
errorContains: "invalid private key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
encrypter := &encrypter{}
|
||||||
|
_, err := encrypter.Encrypt(context.Background(), tt.data, tt.privKey, tt.publicKey)
|
||||||
|
if err != nil && !strings.Contains(err.Error(), tt.errorContains) {
|
||||||
|
t.Errorf("Encrypt() error = %v, want error containing %q", err, tt.errorContains)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNew tests the creation of new encrypter instances.
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success",
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
encrypter, _, err := New(tt.ctx)
|
||||||
|
if err == nil && encrypter == nil {
|
||||||
|
t.Error("New() returned nil encrypter")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pkg/plugin/implementation/reqpreprocessor/cmd/plugin.go
Normal file
24
pkg/plugin/implementation/reqpreprocessor/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/reqpreprocessor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct{}
|
||||||
|
|
||||||
|
func (p provider) New(ctx context.Context, c map[string]string) (func(http.Handler) http.Handler, error) {
|
||||||
|
config := &reqpreprocessor.Config{}
|
||||||
|
if role, ok := c["role"]; ok {
|
||||||
|
config.Role = role
|
||||||
|
}
|
||||||
|
if contextKeys, ok := c["contextKeys"]; ok {
|
||||||
|
config.ContextKeys = strings.Split(contextKeys, ",")
|
||||||
|
}
|
||||||
|
return reqpreprocessor.NewPreProcessor(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Provider = provider{}
|
||||||
122
pkg/plugin/implementation/reqpreprocessor/cmd/plugin_test.go
Normal file
122
pkg/plugin/implementation/reqpreprocessor/cmd/plugin_test.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Will Split this into success and fail (two test cases)
|
||||||
|
func TestProviderNew(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
config map[string]string
|
||||||
|
expectedError bool
|
||||||
|
expectedStatus int
|
||||||
|
prepareRequest func(req *http.Request)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No Config",
|
||||||
|
config: map[string]string{},
|
||||||
|
expectedError: true,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
prepareRequest: func(req *http.Request) {
|
||||||
|
// Add minimal required headers.
|
||||||
|
req.Header.Set("context", "test-context")
|
||||||
|
req.Header.Set("transaction_id", "test-transaction")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Success with BPP role",
|
||||||
|
config: map[string]string{
|
||||||
|
"role": "bpp",
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
prepareRequest: func(req *http.Request) {
|
||||||
|
// Add headers matching the check keys.
|
||||||
|
req.Header.Set("context", "test-context")
|
||||||
|
req.Header.Set("transaction_id", "test-transaction")
|
||||||
|
req.Header.Set("bpp_id", "bpp-456")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing role configuration",
|
||||||
|
config: map[string]string{
|
||||||
|
// No role specified
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
prepareRequest: func(req *http.Request) {
|
||||||
|
req.Header.Set("context", "test-context")
|
||||||
|
req.Header.Set("transaction_id", "test-transaction")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid role configuration",
|
||||||
|
config: map[string]string{
|
||||||
|
"role": "invalid-role",
|
||||||
|
},
|
||||||
|
expectedError: true,
|
||||||
|
prepareRequest: func(req *http.Request) {
|
||||||
|
req.Header.Set("context", "test-context")
|
||||||
|
req.Header.Set("transaction_id", "test-transaction")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "passing the contextKeys",
|
||||||
|
config: map[string]string{
|
||||||
|
"role": "bpp",
|
||||||
|
"contextKeys": "transaction_id,message_id",
|
||||||
|
},
|
||||||
|
expectedError: false,
|
||||||
|
expectedStatus: http.StatusOK,
|
||||||
|
prepareRequest: func(req *http.Request) {
|
||||||
|
req.Header.Set("context", "test-context")
|
||||||
|
req.Header.Set("transaction_id", "test-transaction")
|
||||||
|
req.Header.Set("bpp_id", "bpp1")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
requestBody := `{
|
||||||
|
"context": {
|
||||||
|
"transaction_id": "abc"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
p := provider{}
|
||||||
|
middleware, err := p.New(context.Background(), tc.config)
|
||||||
|
if tc.expectedError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, middleware)
|
||||||
|
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", strings.NewReader(requestBody))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if tc.prepareRequest != nil {
|
||||||
|
tc.prepareRequest(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
middlewaredHandler := middleware(testHandler)
|
||||||
|
middlewaredHandler.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, tc.expectedStatus, w.Code, "Unexpected response status")
|
||||||
|
responseBody := w.Body.String()
|
||||||
|
t.Logf("Response Body: %s", responseBody)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
90
pkg/plugin/implementation/reqpreprocessor/reqpreprocessor.go
Normal file
90
pkg/plugin/implementation/reqpreprocessor/reqpreprocessor.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package reqpreprocessor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the configuration for the request preprocessor middleware.
|
||||||
|
type Config struct {
|
||||||
|
Role string
|
||||||
|
ContextKeys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextKey = "context"
|
||||||
|
|
||||||
|
// NewPreProcessor returns a middleware that processes the incoming request,
|
||||||
|
// extracts the context field from the body, and adds relevant values (like subscriber ID).
|
||||||
|
func NewPreProcessor(cfg *Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
if err := validateConfig(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to read request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req map[string]interface{}
|
||||||
|
ctx := r.Context()
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
http.Error(w, "Failed to decode request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract context from request.
|
||||||
|
reqContext, ok := req["context"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, fmt.Sprintf("%s field not found or invalid.", contextKey), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var subID any
|
||||||
|
switch cfg.Role {
|
||||||
|
case "bap":
|
||||||
|
subID = reqContext["bap_id"]
|
||||||
|
case "bpp":
|
||||||
|
subID = reqContext["bpp_id"]
|
||||||
|
}
|
||||||
|
if subID != nil {
|
||||||
|
log.Debugf(ctx, "adding subscriberId to request:%s, %v", model.ContextKeySubscriberID, subID)
|
||||||
|
ctx = context.WithValue(ctx, model.ContextKeySubscriberID, subID)
|
||||||
|
}
|
||||||
|
for _, key := range cfg.ContextKeys {
|
||||||
|
ctxKey, _ := model.ParseContextKey(key)
|
||||||
|
if v, ok := reqContext[key]; ok {
|
||||||
|
ctx = context.WithValue(ctx, ctxKey, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
|
r.ContentLength = int64(len(body))
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(cfg *Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return errors.New("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Role != "bap" && cfg.Role != "bpp" {
|
||||||
|
return errors.New("role must be either 'bap' or 'bpp'")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range cfg.ContextKeys {
|
||||||
|
if _, err := model.ParseContextKey(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package reqpreprocessor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToDo Separate Middleware creation and execution.
|
||||||
|
func TestNewPreProcessorSuccessCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
requestBody map[string]any
|
||||||
|
expectedID string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "BAP role with valid context",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bap",
|
||||||
|
},
|
||||||
|
requestBody: map[string]interface{}{
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"bap_id": "bap-123",
|
||||||
|
"message_id": "msg-123",
|
||||||
|
},
|
||||||
|
"message": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedID: "bap-123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BPP role with valid context",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bpp",
|
||||||
|
},
|
||||||
|
requestBody: map[string]interface{}{
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"bpp_id": "bpp-456",
|
||||||
|
"message_id": "msg-456",
|
||||||
|
},
|
||||||
|
"message": map[string]interface{}{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedID: "bpp-456",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
middleware, err := NewPreProcessor(tt.config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPreProcessor() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(tt.requestBody)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(bodyBytes))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
var gotSubID interface{}
|
||||||
|
|
||||||
|
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
gotSubID = ctx.Value(model.ContextKeySubscriberID)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
// Verify subscriber ID
|
||||||
|
subID := ctx.Value(model.ContextKeySubscriberID)
|
||||||
|
if subID == nil {
|
||||||
|
t.Errorf("Expected subscriber ID but got none %s", ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the correct ID was set based on role
|
||||||
|
expectedKey := "bap_id"
|
||||||
|
if tt.config.Role == "bpp" {
|
||||||
|
expectedKey = "bpp_id"
|
||||||
|
}
|
||||||
|
expectedID := tt.requestBody["context"].(map[string]interface{})[expectedKey]
|
||||||
|
if subID != expectedID {
|
||||||
|
t.Errorf("Expected subscriber ID %v, got %v", expectedID, subID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware(dummyHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status code 200, but got %d", rec.Code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify subscriber ID
|
||||||
|
if gotSubID == nil {
|
||||||
|
t.Error("Expected subscriber_id to be set in context but got nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subID, ok := gotSubID.(string)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected subscriber_id to be string, got %T", gotSubID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if subID != tt.expectedID {
|
||||||
|
t.Errorf("Expected subscriber_id %q, got %q", tt.expectedID, subID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPreProcessorErrorCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
requestBody interface{}
|
||||||
|
expectedCode int
|
||||||
|
expectErr bool
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Missing context",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bap",
|
||||||
|
},
|
||||||
|
requestBody: map[string]any{
|
||||||
|
"otherKey": "value",
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusBadRequest,
|
||||||
|
expectErr: false,
|
||||||
|
errMsg: "context field not found or invalid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid context type",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bap",
|
||||||
|
},
|
||||||
|
requestBody: map[string]any{
|
||||||
|
"context": "not-a-map",
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusBadRequest,
|
||||||
|
expectErr: false,
|
||||||
|
errMsg: "context field not found or invalid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil config",
|
||||||
|
config: nil,
|
||||||
|
requestBody: map[string]any{},
|
||||||
|
expectedCode: http.StatusInternalServerError,
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "config cannot be nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid role",
|
||||||
|
config: &Config{
|
||||||
|
Role: "invalid-role",
|
||||||
|
},
|
||||||
|
requestBody: map[string]interface{}{
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"bap_id": "bap-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusInternalServerError,
|
||||||
|
expectErr: true,
|
||||||
|
errMsg: "role must be either 'bap' or 'bpp'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing subscriber ID",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bap",
|
||||||
|
},
|
||||||
|
requestBody: map[string]interface{}{
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"message_id": "msg-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid JSON body",
|
||||||
|
config: &Config{
|
||||||
|
Role: "bap",
|
||||||
|
},
|
||||||
|
requestBody: "{invalid-json}",
|
||||||
|
expectedCode: http.StatusBadRequest,
|
||||||
|
expectErr: false,
|
||||||
|
errMsg: "failed to decode request body",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
middleware, err := NewPreProcessor(tt.config)
|
||||||
|
if tt.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected an error for NewPreProcessor(%s), but got none", tt.config)
|
||||||
|
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
|
||||||
|
t.Errorf("Expected error to contain %q, got %v", tt.errMsg, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error while creating middleware: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, _ := json.Marshal(tt.requestBody)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(bodyBytes))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
dummyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
middleware(dummyHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != tt.expectedCode {
|
||||||
|
t.Errorf("Expected status code %d, but got %d", tt.expectedCode, rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPreProcessorAddsSubscriberIDToContext(t *testing.T) {
|
||||||
|
cfg := &Config{Role: "bap"}
|
||||||
|
middleware, err := NewPreProcessor(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
samplePayload := map[string]interface{}{
|
||||||
|
"context": map[string]interface{}{
|
||||||
|
"bap_id": "bap.example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bodyBytes, _ := json.Marshal(samplePayload)
|
||||||
|
|
||||||
|
var receivedSubscriberID interface{}
|
||||||
|
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedSubscriberID = r.Context().Value(model.ContextKeySubscriberID)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest("POST", "/", strings.NewReader(string(bodyBytes)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
middleware(handler).ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("Expected status 200 OK, got %d", rr.Code)
|
||||||
|
}
|
||||||
|
if receivedSubscriberID != "bap.example.com" {
|
||||||
|
t.Errorf("Expected subscriber ID 'bap.example.com', got %v", receivedSubscriberID)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
pkg/plugin/implementation/router/cmd/plugin.go
Normal file
31
pkg/plugin/implementation/router/cmd/plugin.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RouterProvider provides instances of Router.
|
||||||
|
type RouterProvider struct{}
|
||||||
|
|
||||||
|
// New initializes a new Router instance.
|
||||||
|
func (rp RouterProvider) New(ctx context.Context, config map[string]string) (definition.Router, func() error, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, errors.New("context cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the routingConfig key from the config map
|
||||||
|
routingConfig, ok := config["routingConfig"]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.New("routingConfig is required in the configuration")
|
||||||
|
}
|
||||||
|
return router.New(ctx, &router.Config{
|
||||||
|
RoutingConfig: routingConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider = RouterProvider{}
|
||||||
101
pkg/plugin/implementation/router/cmd/plugin_test.go
Normal file
101
pkg/plugin/implementation/router/cmd/plugin_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestConfig creates a temporary directory and writes a sample routing rules file.
|
||||||
|
func setupTestConfig(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Get project root (assuming testData is in project root)
|
||||||
|
_, filename, _, _ := runtime.Caller(0) // Path to plugin_test.go
|
||||||
|
projectRoot := filepath.Dir(filepath.Dir(filename)) // Move up from cmd/
|
||||||
|
yamlPath := filepath.Join(projectRoot, "testData", "bap_receiver.yaml")
|
||||||
|
|
||||||
|
// Copy to temp file (to test file loading logic)
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
tempPath := filepath.Join(tempDir, "routingRules.yaml")
|
||||||
|
content, err := os.ReadFile(yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read test file: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(tempPath, content, 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouterProviderSuccess tests successful router creation.
|
||||||
|
func TestRouterProviderSuccess(t *testing.T) {
|
||||||
|
rulesFilePath := setupTestConfig(t)
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
provider := RouterProvider{}
|
||||||
|
router, _, err := provider.New(context.Background(), map[string]string{
|
||||||
|
"routingConfig": rulesFilePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if router == nil {
|
||||||
|
t.Error("New() returned nil router, want non-nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouterProviderFailure tests the RouterProvider implementation for failure cases.
|
||||||
|
func TestRouterProviderFailure(t *testing.T) {
|
||||||
|
rulesFilePath := setupTestConfig(t)
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty routing config path",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{
|
||||||
|
"routingConfig": "",
|
||||||
|
},
|
||||||
|
wantErr: "failed to load routing rules: routingConfig path is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing routing config key",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
wantErr: "routingConfig is required in the configuration",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil context",
|
||||||
|
ctx: nil,
|
||||||
|
config: map[string]string{"routingConfig": rulesFilePath},
|
||||||
|
wantErr: "context cannot be nil",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
provider := RouterProvider{}
|
||||||
|
_, _, err := provider.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
// Check for expected error
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("New(%v, %v) = nil error, want error containing %q", tt.ctx, tt.config, tt.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Errorf("New(%v, %v) = %v, want error containing %q", tt.ctx, tt.config, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
250
pkg/plugin/implementation/router/router.go
Normal file
250
pkg/plugin/implementation/router/router.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds the configuration for the Router plugin.
|
||||||
|
type Config struct {
|
||||||
|
RoutingConfig string `json:"routingConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutingConfig represents the structure of the routing configuration file.
|
||||||
|
type routingConfig struct {
|
||||||
|
RoutingRules []routingRule `yaml:"routingRules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router implements Router interface.
|
||||||
|
type Router struct {
|
||||||
|
rules map[string]map[string]map[string]*model.Route // domain -> version -> endpoint -> route
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutingRule represents a single routing rule.
|
||||||
|
type routingRule struct {
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
TargetType string `yaml:"targetType"` // "url", "publisher", "bpp", or "bap"
|
||||||
|
Target target `yaml:"target,omitempty"`
|
||||||
|
Endpoints []string `yaml:"endpoints"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target contains destination-specific details.
|
||||||
|
type target struct {
|
||||||
|
URL string `yaml:"url,omitempty"` // URL for "url" or gateway endpoint for "bpp"/"bap"
|
||||||
|
PublisherID string `yaml:"publisherId,omitempty"` // For "msgq" type
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetType defines possible target destinations.
|
||||||
|
const (
|
||||||
|
targetTypeURL = "url" // Route to a specific URL
|
||||||
|
targetTypePublisher = "publisher" // Route to a publisher
|
||||||
|
targetTypeBPP = "bpp" // Route to a BPP endpoint
|
||||||
|
targetTypeBAP = "bap" // Route to a BAP endpoint
|
||||||
|
)
|
||||||
|
|
||||||
|
// New initializes a new Router instance with the provided configuration.
|
||||||
|
// It loads and validates the routing rules from the specified YAML file.
|
||||||
|
// Returns an error if the configuration is invalid or the rules cannot be loaded.
|
||||||
|
func New(ctx context.Context, config *Config) (*Router, func() error, error) {
|
||||||
|
// Check if config is nil
|
||||||
|
if config == nil {
|
||||||
|
return nil, nil, fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
router := &Router{
|
||||||
|
rules: make(map[string]map[string]map[string]*model.Route),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load rules at bootup
|
||||||
|
if err := router.loadRules(config.RoutingConfig); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to load routing rules: %w", err)
|
||||||
|
}
|
||||||
|
return router, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRules reads and parses routing rules from the YAML configuration file.
|
||||||
|
func (r *Router) loadRules(configPath string) error {
|
||||||
|
if configPath == "" {
|
||||||
|
return fmt.Errorf("routingConfig path is empty")
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading config file at %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
var config routingConfig
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return fmt.Errorf("error parsing YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rules
|
||||||
|
if err := validateRules(config.RoutingRules); err != nil {
|
||||||
|
return fmt.Errorf("invalid routing rules: %w", err)
|
||||||
|
}
|
||||||
|
// Build the optimized rule map
|
||||||
|
for _, rule := range config.RoutingRules {
|
||||||
|
// Initialize domain map if not exists
|
||||||
|
if _, ok := r.rules[rule.Domain]; !ok {
|
||||||
|
r.rules[rule.Domain] = make(map[string]map[string]*model.Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize version map if not exists
|
||||||
|
if _, ok := r.rules[rule.Domain][rule.Version]; !ok {
|
||||||
|
r.rules[rule.Domain][rule.Version] = make(map[string]*model.Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all endpoints for this rule
|
||||||
|
for _, endpoint := range rule.Endpoints {
|
||||||
|
var route *model.Route
|
||||||
|
switch rule.TargetType {
|
||||||
|
case targetTypePublisher:
|
||||||
|
route = &model.Route{
|
||||||
|
TargetType: rule.TargetType,
|
||||||
|
PublisherID: rule.Target.PublisherID,
|
||||||
|
}
|
||||||
|
case targetTypeURL:
|
||||||
|
parsedURL, err := url.Parse(rule.Target.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid URL in rule: %w", err)
|
||||||
|
}
|
||||||
|
route = &model.Route{
|
||||||
|
TargetType: rule.TargetType,
|
||||||
|
URL: parsedURL,
|
||||||
|
}
|
||||||
|
case targetTypeBPP, targetTypeBAP:
|
||||||
|
var parsedURL *url.URL
|
||||||
|
if rule.Target.URL != "" {
|
||||||
|
parsedURL, err = url.Parse(rule.Target.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid URL in rule: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
route = &model.Route{
|
||||||
|
TargetType: rule.TargetType,
|
||||||
|
URL: parsedURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.rules[rule.Domain][rule.Version][endpoint] = route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateRules performs basic validation on the loaded routing rules.
|
||||||
|
func validateRules(rules []routingRule) error {
|
||||||
|
for _, rule := range rules {
|
||||||
|
// Ensure domain, version, and TargetType are present
|
||||||
|
if rule.Domain == "" || rule.Version == "" || rule.TargetType == "" {
|
||||||
|
return fmt.Errorf("invalid rule: domain, version, and targetType are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate based on TargetType
|
||||||
|
switch rule.TargetType {
|
||||||
|
case targetTypeURL:
|
||||||
|
if rule.Target.URL == "" {
|
||||||
|
return fmt.Errorf("invalid rule: url is required for targetType 'url'")
|
||||||
|
}
|
||||||
|
if _, err := url.Parse(rule.Target.URL); err != nil {
|
||||||
|
return fmt.Errorf("invalid URL - %s: %w", rule.Target.URL, err)
|
||||||
|
}
|
||||||
|
case targetTypePublisher:
|
||||||
|
if rule.Target.PublisherID == "" {
|
||||||
|
return fmt.Errorf("invalid rule: publisherID is required for targetType 'publisher'")
|
||||||
|
}
|
||||||
|
case targetTypeBPP, targetTypeBAP:
|
||||||
|
if rule.Target.URL != "" {
|
||||||
|
if _, err := url.Parse(rule.Target.URL); err != nil {
|
||||||
|
return fmt.Errorf("invalid URL - %s defined in routing config for target type %s: %w", rule.Target.URL, rule.TargetType, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid rule: unknown targetType '%s'", rule.TargetType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route determines the routing destination based on the request context.
|
||||||
|
func (r *Router) Route(ctx context.Context, url *url.URL, body []byte) (*model.Route, error) {
|
||||||
|
// Parse the body to extract domain and version
|
||||||
|
var requestBody struct {
|
||||||
|
Context struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
BPPURI string `json:"bpp_uri,omitempty"`
|
||||||
|
BAPURI string `json:"bap_uri,omitempty"`
|
||||||
|
} `json:"context"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &requestBody); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing request body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the endpoint from the URL
|
||||||
|
endpoint := path.Base(url.Path)
|
||||||
|
|
||||||
|
// Lookup route in the optimized map
|
||||||
|
domainRules, ok := r.rules[requestBody.Context.Domain]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no routing rules found for domain %s", requestBody.Context.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionRules, ok := domainRules[requestBody.Context.Version]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no routing rules found for domain %s version %s", requestBody.Context.Domain, requestBody.Context.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
route, ok := versionRules[endpoint]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("endpoint '%s' is not supported for domain %s and version %s in routing config",
|
||||||
|
endpoint, requestBody.Context.Domain, requestBody.Context.Version)
|
||||||
|
}
|
||||||
|
// Handle BPP/BAP routing with request URIs
|
||||||
|
switch route.TargetType {
|
||||||
|
case targetTypeBPP:
|
||||||
|
return handleProtocolMapping(route, requestBody.Context.BPPURI, endpoint)
|
||||||
|
case targetTypeBAP:
|
||||||
|
return handleProtocolMapping(route, requestBody.Context.BAPURI, endpoint)
|
||||||
|
}
|
||||||
|
return route, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleProtocolMapping handles both BPP and BAP routing with proper URL construction
|
||||||
|
func handleProtocolMapping(route *model.Route, npURI, endpoint string) (*model.Route, error) {
|
||||||
|
target := strings.TrimSpace(npURI)
|
||||||
|
if len(target) == 0 {
|
||||||
|
if route.URL == nil {
|
||||||
|
return nil, fmt.Errorf("could not determine destination for endpoint '%s': neither request contained a %s URI nor was a default URL configured in routing rules", endpoint, strings.ToUpper(route.TargetType))
|
||||||
|
}
|
||||||
|
return &model.Route{
|
||||||
|
TargetType: targetTypeURL,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: route.URL.Scheme,
|
||||||
|
Host: route.URL.Host,
|
||||||
|
Path: path.Join(route.URL.Path, endpoint),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
targetURL, err := url.Parse(target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid %s URI - %s in request body for %s: %w", strings.ToUpper(route.TargetType), target, endpoint, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Route{
|
||||||
|
TargetType: targetTypeURL,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: targetURL.Scheme,
|
||||||
|
Host: targetURL.Host,
|
||||||
|
Path: path.Join(targetURL.Path, endpoint),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
486
pkg/plugin/implementation/router/router_test.go
Normal file
486
pkg/plugin/implementation/router/router_test.go
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed testData/*
|
||||||
|
var testData embed.FS
|
||||||
|
|
||||||
|
func setupTestConfig(t *testing.T, yamlFileName string) string {
|
||||||
|
t.Helper()
|
||||||
|
configDir := t.TempDir()
|
||||||
|
|
||||||
|
content, err := testData.ReadFile("testData/" + yamlFileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile() err = %v, want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesPath := filepath.Join(configDir, "routing_rules.yaml")
|
||||||
|
if err := os.WriteFile(rulesPath, content, 0644); err != nil {
|
||||||
|
t.Fatalf("WriteFile() err = %v, want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupRouter is a helper function to create router instance.
|
||||||
|
func setupRouter(t *testing.T, configFile string) (*Router, func() error, string) {
|
||||||
|
rulesFilePath := setupTestConfig(t, configFile)
|
||||||
|
config := &Config{
|
||||||
|
RoutingConfig: rulesFilePath,
|
||||||
|
}
|
||||||
|
router, _, err := New(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New failed: %v", err)
|
||||||
|
}
|
||||||
|
return router, nil, rulesFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNew tests the New function.
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// List of YAML files in the testData directory
|
||||||
|
yamlFiles := []string{
|
||||||
|
"bap_caller.yaml",
|
||||||
|
"bap_receiver.yaml",
|
||||||
|
"bpp_caller.yaml",
|
||||||
|
"bpp_receiver.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, yamlFile := range yamlFiles {
|
||||||
|
t.Run(yamlFile, func(t *testing.T) {
|
||||||
|
rulesFilePath := setupTestConfig(t, yamlFile)
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
// Define test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid configuration",
|
||||||
|
config: &Config{
|
||||||
|
RoutingConfig: rulesFilePath,
|
||||||
|
},
|
||||||
|
wantErr: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty config",
|
||||||
|
config: nil,
|
||||||
|
wantErr: "config cannot be nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty routing config path",
|
||||||
|
config: &Config{
|
||||||
|
RoutingConfig: "",
|
||||||
|
},
|
||||||
|
wantErr: "routingConfig path is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Routing config file does not exist",
|
||||||
|
config: &Config{
|
||||||
|
RoutingConfig: "/nonexistent/path/to/rules.yaml",
|
||||||
|
},
|
||||||
|
wantErr: "error reading config file",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
router, _, err := New(ctx, tt.config)
|
||||||
|
|
||||||
|
// Check for expected error
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Errorf("New(%v) = %v, want error containing %q", tt.config, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no error occurred
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("New(%v) = %v, want nil error", tt.config, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the router and close function are not nil
|
||||||
|
if router == nil {
|
||||||
|
t.Errorf("New(%v, %v) = nil router, want non-nil", ctx, tt.config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidateRulesSuccess tests the validate function for success cases.
|
||||||
|
func TestValidateRulesSuccess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rules []routingRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid rules with url routing",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "url",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://example.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"on_search", "on_select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid rules with publisher routing",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "publisher",
|
||||||
|
Target: target{
|
||||||
|
PublisherID: "example_topic",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"on_search", "on_select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid rules with bpp routing to gateway",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "bpp",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://mock_gateway.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid rules with bpp routing",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "bpp",
|
||||||
|
Endpoints: []string{"select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid rules with bap routing",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "bap",
|
||||||
|
Endpoints: []string{"select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateRules(tt.rules)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("validateRules(%v) = %v, want nil error", tt.rules, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidateRulesFailure tests the validate function for failure cases.
|
||||||
|
func TestValidateRulesFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
rules []routingRule
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Missing domain",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "url",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://example.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: domain, version, and targetType are required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing version",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
TargetType: "url",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://example.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: domain, version, and targetType are required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing targetType",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://example.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: domain, version, and targetType are required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid targetType",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "invalid",
|
||||||
|
Target: target{
|
||||||
|
URL: "https://example.com/api",
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: unknown targetType 'invalid'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing url for targetType: url",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "url",
|
||||||
|
Target: target{
|
||||||
|
// URL is missing
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: url is required for targetType 'url'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid URL format for targetType: url",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "url",
|
||||||
|
Target: target{
|
||||||
|
URL: "htp:// invalid-url.com", // Invalid scheme
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: `invalid URL - htp:// invalid-url.com: parse "htp:// invalid-url.com": invalid character " " in host name`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing topic_id for targetType: publisher",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "publisher",
|
||||||
|
Target: target{
|
||||||
|
// PublisherID is missing
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search", "select"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: "invalid rule: publisherID is required for targetType 'publisher'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid URL for BPP targetType",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "bpp",
|
||||||
|
Target: target{
|
||||||
|
URL: "htp:// invalid-url.com", // Invalid URL
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: `invalid URL - htp:// invalid-url.com defined in routing config for target type bpp: parse "htp:// invalid-url.com": invalid character " " in host name`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid URL for BAP targetType",
|
||||||
|
rules: []routingRule{
|
||||||
|
{
|
||||||
|
Domain: "retail",
|
||||||
|
Version: "1.0.0",
|
||||||
|
TargetType: "bap",
|
||||||
|
Target: target{
|
||||||
|
URL: "http:// [invalid].com", // Invalid host
|
||||||
|
},
|
||||||
|
Endpoints: []string{"search"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: `invalid URL - http:// [invalid].com defined in routing config for target type bap: parse "http:// [invalid].com": invalid character " " in host name`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateRules(tt.rules)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Errorf("validateRules(%v) = %v, want error containing %q", tt.rules, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouteSuccess tests the Route function for success cases.
|
||||||
|
func TestRouteSuccess(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Define success test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configFile string
|
||||||
|
url string
|
||||||
|
body string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (bpp routing with gateway URL)",
|
||||||
|
configFile: "bap_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/search",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)",
|
||||||
|
configFile: "bap_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (url routing)",
|
||||||
|
configFile: "bpp_receiver.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (publisher routing)",
|
||||||
|
configFile: "bpp_receiver.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/search",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (bap routing with bap_uri)",
|
||||||
|
configFile: "bpp_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/on_select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bap_uri": "https://bap1.example.com"}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid domain, version, and endpoint (bpp routing with bpp_uri)",
|
||||||
|
configFile: "bap_receiver.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/on_select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "https://bpp1.example.com"}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
router, _, rulesFilePath := setupRouter(t, tt.configFile)
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
parsedURL, _ := url.Parse(tt.url)
|
||||||
|
_, err := router.Route(ctx, parsedURL, []byte(tt.body))
|
||||||
|
|
||||||
|
// Ensure no error occurred
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("router.Route(%v, %v, %v) = %v, want nil error", ctx, parsedURL, []byte(tt.body), err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRouteFailure tests the Route function for failure cases.
|
||||||
|
func TestRouteFailure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Define failure test cases
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
configFile string
|
||||||
|
url string
|
||||||
|
body string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Unsupported endpoint",
|
||||||
|
configFile: "bpp_receiver.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/unsupported",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV11", "version": "2.0.0"}}`,
|
||||||
|
wantErr: "endpoint 'unsupported' is not supported for domain ONDC:TRV11 and version 2.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No matching rule",
|
||||||
|
configFile: "bpp_receiver.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:SRV11", "version": "2.0.0"}}`,
|
||||||
|
wantErr: "no routing rules found for domain ONDC:SRV11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing bap_uri for bap routing",
|
||||||
|
configFile: "bpp_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/on_search",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
|
||||||
|
wantErr: "could not determine destination for endpoint 'on_search': neither request contained a BAP URI nor was a default URL configured in routing rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing bpp_uri for bpp routing",
|
||||||
|
configFile: "bap_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0"}}`,
|
||||||
|
wantErr: "could not determine destination for endpoint 'select': neither request contained a BPP URI nor was a default URL configured in routing rules",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid bpp_uri format in request",
|
||||||
|
configFile: "bap_caller.yaml",
|
||||||
|
url: "https://example.com/v1/ondc/select",
|
||||||
|
body: `{"context": {"domain": "ONDC:TRV10", "version": "2.0.0", "bpp_uri": "htp:// invalid-url"}}`, // Invalid scheme (htp instead of http)
|
||||||
|
wantErr: `invalid BPP URI - htp:// invalid-url in request body for select: parse "htp:// invalid-url": invalid character " " in host name`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
router, _, rulesFilePath := setupRouter(t, tt.configFile)
|
||||||
|
defer os.RemoveAll(filepath.Dir(rulesFilePath))
|
||||||
|
|
||||||
|
parsedURL, _ := url.Parse(tt.url)
|
||||||
|
_, err := router.Route(ctx, parsedURL, []byte(tt.body))
|
||||||
|
|
||||||
|
// Check for expected error
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Errorf("Route(%q, %q) = %v, want error containing %q", tt.url, tt.body, err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
25
pkg/plugin/implementation/router/testData/bap_caller.yaml
Normal file
25
pkg/plugin/implementation/router/testData/bap_caller.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: bpp
|
||||||
|
target:
|
||||||
|
url: https://gateway.example.com
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: bpp
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
- domain: ONDC:TRV12
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: bpp
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
20
pkg/plugin/implementation/router/testData/bap_receiver.yaml
Normal file
20
pkg/plugin/implementation/router/testData/bap_receiver.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: url
|
||||||
|
target:
|
||||||
|
url: https://services-backend/trv/v1
|
||||||
|
endpoints:
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: publisher
|
||||||
|
target:
|
||||||
|
publisherId: trv_topic_id1
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
23
pkg/plugin/implementation/router/testData/bpp_caller.yaml
Normal file
23
pkg/plugin/implementation/router/testData/bpp_caller.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: bap
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
|
- domain: ONDC:TRV11
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: bap
|
||||||
|
endpoints:
|
||||||
|
- on_search
|
||||||
|
- on_select
|
||||||
|
- on_init
|
||||||
|
- on_confirm
|
||||||
|
- on_status
|
||||||
|
- on_update
|
||||||
|
- on_cancel
|
||||||
28
pkg/plugin/implementation/router/testData/bpp_receiver.yaml
Normal file
28
pkg/plugin/implementation/router/testData/bpp_receiver.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
routingRules:
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: url
|
||||||
|
target:
|
||||||
|
url: https://services-backend/trv/v1
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
|
- status
|
||||||
|
- cancel
|
||||||
|
- domain: ONDC:TRV10
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: publisher
|
||||||
|
target:
|
||||||
|
publisherId: trv_topic_id1
|
||||||
|
endpoints:
|
||||||
|
- search
|
||||||
|
- domain: ONDC:TRV11
|
||||||
|
version: 2.0.0
|
||||||
|
targetType: url
|
||||||
|
target:
|
||||||
|
url: https://services-backend/trv/v1
|
||||||
|
endpoints:
|
||||||
|
- select
|
||||||
|
- init
|
||||||
|
- confirm
|
||||||
Binary file not shown.
33
pkg/plugin/implementation/schemavalidator/cmd/plugin.go
Normal file
33
pkg/plugin/implementation/schemavalidator/cmd/plugin.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/schemavalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// schemaValidatorProvider provides instances of schemaValidator.
|
||||||
|
type schemaValidatorProvider struct{}
|
||||||
|
|
||||||
|
// New initializes a new Verifier instance.
|
||||||
|
func (vp schemaValidatorProvider) New(ctx context.Context, config map[string]string) (definition.SchemaValidator, func() error, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, errors.New("context cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract schemaDir from the config map
|
||||||
|
schemaDir, ok := config["schemaDir"]
|
||||||
|
if !ok || schemaDir == "" {
|
||||||
|
return nil, nil, errors.New("config must contain 'schemaDir'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = schemaValidatorProvider{}
|
||||||
150
pkg/plugin/implementation/schemavalidator/cmd/plugin_test.go
Normal file
150
pkg/plugin/implementation/schemavalidator/cmd/plugin_test.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestSchema creates a temporary directory and writes a sample schema file.
|
||||||
|
func setupTestSchema(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create a temporary directory for the schema
|
||||||
|
schemaDir, err := os.MkdirTemp("", "schemas")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the directory structure for the schema file
|
||||||
|
schemaFilePath := filepath.Join(schemaDir, "example", "1.0", "test_schema.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(schemaFilePath), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create schema directory structure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a sample schema
|
||||||
|
schemaContent := `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"version": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["domain", "version"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["context"]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Write the schema to the file
|
||||||
|
if err := os.WriteFile(schemaFilePath, []byte(schemaContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write schema file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidatorProviderSuccess tests successful ValidatorProvider implementation.
|
||||||
|
func TestValidatorProviderSuccess(t *testing.T) {
|
||||||
|
schemaDir := setupTestSchema(t)
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
// Define test cases.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid schema directory",
|
||||||
|
ctx: context.Background(), // Valid context
|
||||||
|
config: map[string]string{"schemaDir": schemaDir},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test using table-driven tests
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
vp := schemaValidatorProvider{}
|
||||||
|
schemaValidator, _, err := vp.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
// Ensure no error occurred
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the schemaValidator is not nil
|
||||||
|
if schemaValidator == nil {
|
||||||
|
t.Error("expected a non-nil schemaValidator, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestValidatorProviderSuccess tests cases where ValidatorProvider creation should fail.
|
||||||
|
func TestValidatorProviderFailure(t *testing.T) {
|
||||||
|
schemaDir := setupTestSchema(t)
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
// Define test cases.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]string
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Config is empty",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{},
|
||||||
|
expectedError: "config must contain 'schemaDir'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "schemaDir is empty",
|
||||||
|
ctx: context.Background(),
|
||||||
|
config: map[string]string{"schemaDir": ""},
|
||||||
|
expectedError: "config must contain 'schemaDir'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid schema directory",
|
||||||
|
ctx: context.Background(), // Valid context
|
||||||
|
config: map[string]string{"schemaDir": "/invalid/dir"},
|
||||||
|
expectedError: "failed to initialise schemaValidator: schema directory does not exist: /invalid/dir",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil context",
|
||||||
|
ctx: nil, // Nil context
|
||||||
|
config: map[string]string{"schemaDir": schemaDir},
|
||||||
|
expectedError: "context cannot be nil",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test using table-driven tests
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
vp := schemaValidatorProvider{}
|
||||||
|
_, _, err := vp.New(tt.ctx, tt.config)
|
||||||
|
|
||||||
|
// Check for expected error
|
||||||
|
if tt.expectedError != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
|
||||||
|
t.Errorf("expected error %q, got %v", tt.expectedError, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no error occurred
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
193
pkg/plugin/implementation/schemavalidator/schemavalidator.go
Normal file
193
pkg/plugin/implementation/schemavalidator/schemavalidator.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package schemavalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Payload represents the structure of the data payload with context information.
|
||||||
|
type payload struct {
|
||||||
|
Context struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
} `json:"context"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// schemaValidator implements the Validator interface.
|
||||||
|
type schemaValidator struct {
|
||||||
|
config *Config
|
||||||
|
schemaCache map[string]*jsonschema.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config struct for SchemaValidator.
|
||||||
|
type Config struct {
|
||||||
|
SchemaDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new ValidatorProvider instance.
|
||||||
|
func New(ctx context.Context, config *Config) (*schemaValidator, func() error, error) {
|
||||||
|
// Check if config is nil
|
||||||
|
if config == nil {
|
||||||
|
return nil, nil, fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
v := &schemaValidator{
|
||||||
|
config: config,
|
||||||
|
schemaCache: make(map[string]*jsonschema.Schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Initialise function to load schemas and get validators
|
||||||
|
if err := v.initialise(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to initialise schemaValidator: %v", err)
|
||||||
|
}
|
||||||
|
return v, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the given data against the schema.
|
||||||
|
func (v *schemaValidator) Validate(ctx context.Context, url *url.URL, data []byte) error {
|
||||||
|
var payloadData payload
|
||||||
|
err := json.Unmarshal(data, &payloadData)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewBadReqErr(fmt.Errorf("failed to parse JSON payload: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract domain, version, and endpoint from the payload and uri.
|
||||||
|
cxtDomain := payloadData.Context.Domain
|
||||||
|
version := payloadData.Context.Version
|
||||||
|
version = fmt.Sprintf("v%s", version)
|
||||||
|
|
||||||
|
endpoint := path.Base(url.String())
|
||||||
|
log.Debugf(ctx, "Handling request for endpoint: %s", endpoint)
|
||||||
|
domain := strings.ToLower(cxtDomain)
|
||||||
|
domain = strings.ReplaceAll(domain, ":", "_")
|
||||||
|
|
||||||
|
// Construct the schema file name.
|
||||||
|
schemaFileName := fmt.Sprintf("%s_%s_%s", domain, version, endpoint)
|
||||||
|
|
||||||
|
// Retrieve the schema from the cache.
|
||||||
|
schema, exists := v.schemaCache[schemaFileName]
|
||||||
|
if !exists {
|
||||||
|
return model.NewBadReqErr(fmt.Errorf("schema not found for domain: %s", domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData any
|
||||||
|
if err := json.Unmarshal(data, &jsonData); err != nil {
|
||||||
|
return model.NewBadReqErr(fmt.Errorf("failed to parse JSON data: %v", err))
|
||||||
|
}
|
||||||
|
err = schema.Validate(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
// Handle schema validation errors
|
||||||
|
if validationErr, ok := err.(*jsonschema.ValidationError); ok {
|
||||||
|
// Convert validation errors into an array of SchemaValError
|
||||||
|
var schemaErrors []model.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, model.Error{
|
||||||
|
Paths: path,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Return the array of schema validation errors
|
||||||
|
return &model.SchemaValidationErr{Errors: schemaErrors}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("validation failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil if validation succeeds
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
schemaDir := v.config.SchemaDir
|
||||||
|
// Check if the directory exists and is accessible.
|
||||||
|
info, err := os.Stat(schemaDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("schema directory does not exist: %s", schemaDir)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to access schema directory: %v", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return fmt.Errorf("provided schema path is not a directory: %s", schemaDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
compiler := jsonschema.NewCompiler()
|
||||||
|
|
||||||
|
// Helper function to process directories recursively.
|
||||||
|
var processDir func(dir string) error
|
||||||
|
processDir = func(dir string) error {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
path := filepath.Join(dir, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
|
// Recursively process subdirectories.
|
||||||
|
if err := processDir(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if filepath.Ext(entry.Name()) == ".json" {
|
||||||
|
// Process JSON files.
|
||||||
|
compiledSchema, err := compiler.Compile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to compile JSON schema from file %s: %v", entry.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use relative path from schemaDir to avoid absolute paths and make schema keys domain/version specific.
|
||||||
|
relativePath, err := filepath.Rel(schemaDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get relative path for file %s: %v", entry.Name(), err)
|
||||||
|
}
|
||||||
|
// Split the relative path to get domain, version, and schema.
|
||||||
|
parts := strings.Split(relativePath, string(os.PathSeparator))
|
||||||
|
|
||||||
|
// Ensure that the file path has at least 3 parts: domain, version, and schema file.
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return fmt.Errorf("invalid schema file structure, expected domain/version/schema.json but got: %s", relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract domain, version, and schema filename from the parts.
|
||||||
|
// Validate that the extracted parts are non-empty.
|
||||||
|
domain := strings.TrimSpace(parts[0])
|
||||||
|
version := strings.TrimSpace(parts[1])
|
||||||
|
schemaFileName := strings.TrimSpace(parts[2])
|
||||||
|
schemaFileName = strings.TrimSuffix(schemaFileName, ".json")
|
||||||
|
|
||||||
|
if domain == "" || version == "" || schemaFileName == "" {
|
||||||
|
return fmt.Errorf("invalid schema file structure, one or more components are empty. Relative path: %s", relativePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a unique key combining domain, version, and schema name (e.g., ondc_trv10_v2.0.0_schema).
|
||||||
|
uniqueKey := fmt.Sprintf("%s_%s_%s", domain, version, schemaFileName)
|
||||||
|
// Store the compiled schema in the SchemaCache using the unique key.
|
||||||
|
v.schemaCache[uniqueKey] = compiledSchema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start processing from the root schema directory.
|
||||||
|
if err := processDir(schemaDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to read schema directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
package schemavalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v6"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTestSchema creates a temporary directory and writes a sample schema file.
|
||||||
|
func setupTestSchema(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Create a temporary directory for the schema
|
||||||
|
schemaDir, err := os.MkdirTemp("", "schemas")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the directory structure for the schema file
|
||||||
|
schemaFilePath := filepath.Join(schemaDir, "example", "v1.0", "endpoint.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(schemaFilePath), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create schema directory structure: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define a sample schema
|
||||||
|
schemaContent := `{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"version": {"type": "string"},
|
||||||
|
"action": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["domain", "version", "action"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["context"]
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Write the schema to the file
|
||||||
|
if err := os.WriteFile(schemaFilePath, []byte(schemaContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write schema file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidator_Validate_Success(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
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"}}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup a temporary schema directory for testing
|
||||||
|
schemaDir := setupTestSchema(t)
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
config := &Config{SchemaDir: schemaDir}
|
||||||
|
v, _, err := New(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create validator: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
u, _ := url.Parse(tt.url)
|
||||||
|
err := v.Validate(context.Background(), u, []byte(tt.payload))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("Test %s passed with no errors", tt.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidator_Validate_Failure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
payload string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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"}}`,
|
||||||
|
wantErr: "context: at '/context': missing property 'action'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Schema not found",
|
||||||
|
url: "http://example.com/unknown_endpoint",
|
||||||
|
payload: `{"context": {"domain": "example", "version": "1.0"}}`,
|
||||||
|
wantErr: "schema not found for domain",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup a temporary schema directory for testing
|
||||||
|
schemaDir := setupTestSchema(t)
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
config := &Config{SchemaDir: schemaDir}
|
||||||
|
v, _, err := New(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create validator: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
u, _ := url.Parse(tt.url)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidator_Initialise(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupFunc func(schemaDir string) error
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Schema directory does not exist",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Do not create the schema directory
|
||||||
|
return nil
|
||||||
|
|
||||||
|
},
|
||||||
|
wantErr: "schema directory does not exist",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Schema path is not a directory",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create a file instead of a directory
|
||||||
|
return os.WriteFile(schemaDir, []byte{}, 0644)
|
||||||
|
},
|
||||||
|
wantErr: "provided schema path is not a directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid schema file structure",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create an invalid schema file structure
|
||||||
|
invalidSchemaFile := filepath.Join(schemaDir, "invalid_schema.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(invalidSchemaFile, []byte(`{}`), 0644)
|
||||||
|
},
|
||||||
|
wantErr: "invalid schema file structure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed to compile JSON schema",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create a schema file with invalid JSON
|
||||||
|
invalidSchemaFile := filepath.Join(schemaDir, "example", "1.0", "endpoint.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(invalidSchemaFile, []byte(`{invalid json}`), 0644)
|
||||||
|
},
|
||||||
|
wantErr: "failed to compile JSON schema",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid schema file structure with empty components",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create a schema file with empty domain, version, or schema name
|
||||||
|
invalidSchemaFile := filepath.Join(schemaDir, "", "1.0", "endpoint.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(invalidSchemaFile), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(invalidSchemaFile, []byte(`{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"version": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["domain", "version"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["context"]
|
||||||
|
}`), 0644)
|
||||||
|
},
|
||||||
|
wantErr: "failed to read schema directory: invalid schema file structure, expected domain/version/schema.json but got: 1.0/endpoint.json",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed to read directory",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create a directory and remove read permissions
|
||||||
|
if err := os.MkdirAll(schemaDir, 0000); err != nil {
|
||||||
|
t.Fatalf("Failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
wantErr: "failed to read directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid schema directory",
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Create a valid schema file
|
||||||
|
validSchemaFile := filepath.Join(schemaDir, "example", "1.0", "endpoint.json")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(validSchemaFile), 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
return os.WriteFile(validSchemaFile, []byte(`{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"context": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"domain": {"type": "string"},
|
||||||
|
"version": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["domain", "version"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["context"]
|
||||||
|
}`), 0644)
|
||||||
|
},
|
||||||
|
wantErr: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Setup a temporary schema directory for testing
|
||||||
|
schemaDir := filepath.Join(os.TempDir(), "schemas")
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
// Run the setup function to prepare the test case
|
||||||
|
if err := tt.setupFunc(schemaDir); err != nil {
|
||||||
|
t.Fatalf("setupFunc() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{SchemaDir: schemaDir}
|
||||||
|
v := &schemaValidator{
|
||||||
|
config: config,
|
||||||
|
schemaCache: make(map[string]*jsonschema.Schema),
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else if err == nil {
|
||||||
|
t.Logf("Test %s passed: validator initialized successfully", tt.name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Test %s passed with expected error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorNew_Success(t *testing.T) {
|
||||||
|
schemaDir := setupTestSchema(t)
|
||||||
|
defer os.RemoveAll(schemaDir)
|
||||||
|
|
||||||
|
config := &Config{SchemaDir: schemaDir}
|
||||||
|
_, _, err := New(context.Background(), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatorNewFailure(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config *Config
|
||||||
|
setupFunc func(schemaDir string) error
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Config is nil",
|
||||||
|
config: nil,
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
wantErr: "config cannot be nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failed to initialise validators",
|
||||||
|
config: &Config{
|
||||||
|
SchemaDir: "/invalid/path",
|
||||||
|
},
|
||||||
|
setupFunc: func(schemaDir string) error {
|
||||||
|
// Do not create the schema directory
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
wantErr: "ailed to initialise schemaValidator: schema directory does not exist: /invalid/path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Run the setup function if provided
|
||||||
|
if tt.setupFunc != nil {
|
||||||
|
schemaDir := ""
|
||||||
|
if tt.config != nil {
|
||||||
|
schemaDir = tt.config.SchemaDir
|
||||||
|
}
|
||||||
|
if err := tt.setupFunc(schemaDir); err != nil {
|
||||||
|
t.Fatalf("Setup function failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the New function with the test config
|
||||||
|
_, _, err := New(context.Background(), tt.config)
|
||||||
|
if (err != nil && !strings.Contains(err.Error(), tt.wantErr)) || (err == nil && tt.wantErr != "") {
|
||||||
|
t.Errorf("Error: New() returned error = %v, expected error = %v", err, tt.wantErr)
|
||||||
|
} else {
|
||||||
|
t.Logf("Test %s passed with expected error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/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 = SignerProvider{}
|
||||||
101
pkg/plugin/implementation/signer/cmd/plugin_test.go
Normal file
101
pkg/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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
73
pkg/plugin/implementation/signer/signer.go
Normal file
73
pkg/plugin/implementation/signer/signer.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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, nil, 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.SeedSize {
|
||||||
|
return nil, errors.New("invalid seed length")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the private key from the seed
|
||||||
|
privateKey := ed25519.NewKeyFromSeed(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
|
||||||
|
}
|
||||||
104
pkg/plugin/implementation/signer/signer_test.go
Normal file
104
pkg/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.Seed()), 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 seed 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
pkg/plugin/implementation/signvalidator/cmd/plugin.go
Normal file
24
pkg/plugin/implementation/signvalidator/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/signvalidator"
|
||||||
|
)
|
||||||
|
|
||||||
|
// provider provides instances of Verifier.
|
||||||
|
type provider struct{}
|
||||||
|
|
||||||
|
// New initializes a new Verifier instance.
|
||||||
|
func (vp provider) New(ctx context.Context, config map[string]string) (definition.SignValidator, func() error, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil, nil, errors.New("context cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
return signvalidator.New(ctx, &signvalidator.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider = provider{}
|
||||||
89
pkg/plugin/implementation/signvalidator/cmd/plugin_test.go
Normal file
89
pkg/plugin/implementation/signvalidator/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 := provider{}
|
||||||
|
|
||||||
|
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 := provider{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
110
pkg/plugin/implementation/signvalidator/signvalidator.go
Normal file
110
pkg/plugin/implementation/signvalidator/signvalidator.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package signvalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
"golang.org/x/crypto/blake2b"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config struct for Verifier.
|
||||||
|
type Config struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// validator implements the validator interface.
|
||||||
|
type validator struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Verifier instance.
|
||||||
|
func New(ctx context.Context, config *Config) (*validator, func() error, error) {
|
||||||
|
v := &validator{config: config}
|
||||||
|
|
||||||
|
return v, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks the signature for the given payload and public key.
|
||||||
|
func (v *validator) Validate(ctx context.Context, body []byte, header string, publicKeyBase64 string) error {
|
||||||
|
createdTimestamp, expiredTimestamp, signature, err := parseAuthHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("error parsing header: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
if createdTimestamp > currentTime || currentTime > expiredTimestamp {
|
||||||
|
return model.NewSignValidationErr(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 {
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("error decoding public key: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ed25519.Verify(ed25519.PublicKey(decodedPublicKey), []byte(signingString), signatureBytes) {
|
||||||
|
return model.NewSignValidationErr(fmt.Errorf("signature verification failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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 {
|
||||||
|
return 0, 0, "", model.NewSignValidationErr(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, "", model.NewSignValidationErr(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)
|
||||||
|
}
|
||||||
147
pkg/plugin/implementation/signvalidator/signvalidator_test.go
Normal file
147
pkg/plugin/implementation/signvalidator/signvalidator_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package signvalidator
|
||||||
|
|
||||||
|
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{})
|
||||||
|
err := verifier.Validate(context.Background(), tt.body, header, publicKeyBase64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected no error, but got: %v", err)
|
||||||
|
}
|
||||||
|
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{})
|
||||||
|
err := verifier.Validate(context.Background(), tt.body, tt.header, tt.pubKey)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error but got none")
|
||||||
|
}
|
||||||
|
if close != nil {
|
||||||
|
if err := close(); err != nil {
|
||||||
|
t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
<<<<<<< HEAD
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
=======
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
>>>>>>> fdec61e90d57d3d82345d023c1a0d33d5a90583b
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"plugin"
|
"plugin"
|
||||||
"strings"
|
"strings"
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
|
||||||
@@ -142,4 +152,381 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
|
=======
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
type onixPlugin interface {
|
||||||
|
Lookup(string) (plugin.Symbol, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager is responsible for managing dynamically loaded plugins.
|
||||||
|
type Manager struct {
|
||||||
|
plugins map[string]onixPlugin // plugins holds the dynamically loaded plugins.
|
||||||
|
closers []func() // closers contains functions to release resources when the manager is closed.
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMgrCfg(cfg *ManagerConfig) error {
|
||||||
|
if cfg.Root == "" {
|
||||||
|
return fmt.Errorf("root path cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager initializes a new Manager instance by loading plugins from the specified configuration.
|
||||||
|
func NewManager(ctx context.Context, cfg *ManagerConfig) (*Manager, func(), error) {
|
||||||
|
if err := validateMgrCfg(cfg); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Invalid config: %w", err)
|
||||||
|
}
|
||||||
|
log.Debugf(ctx, "RemoteRoot : %s", cfg.RemoteRoot)
|
||||||
|
if len(cfg.RemoteRoot) != 0 {
|
||||||
|
log.Debugf(ctx, "Unzipping files from : %s to : %s", cfg.RemoteRoot, cfg.Root)
|
||||||
|
if err := unzip(cfg.RemoteRoot, cfg.Root); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugins, err := plugins(ctx, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
closers := []func(){}
|
||||||
|
return &Manager{plugins: plugins, closers: closers}, func() {
|
||||||
|
for _, closer := range closers {
|
||||||
|
closer()
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func plugins(ctx context.Context, cfg *ManagerConfig) (map[string]onixPlugin, error) {
|
||||||
|
plugins := make(map[string]onixPlugin)
|
||||||
|
|
||||||
|
err := filepath.WalkDir(cfg.Root, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil // Skip directories
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(d.Name(), ".so") {
|
||||||
|
id := strings.TrimSuffix(d.Name(), ".so") // Extract plugin ID
|
||||||
|
p, elapsed, err := loadPlugin(ctx, path, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
plugins[id] = p
|
||||||
|
log.Debugf(ctx, "Loaded plugin: %s in %s", id, elapsed)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadPlugin attempts to load a plugin from the given path and logs the execution time.
|
||||||
|
func loadPlugin(ctx context.Context, path, id string) (onixPlugin, time.Duration, error) {
|
||||||
|
log.Debugf(ctx, "Loading plugin: %s", id)
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
p, err := plugin.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to open plugin %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
return p, elapsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func provider[T any](plugins map[string]onixPlugin, id string) (T, error) {
|
||||||
|
var zero T
|
||||||
|
pgn, ok := plugins[id]
|
||||||
|
if !ok {
|
||||||
|
return zero, fmt.Errorf("plugin %s not found", id)
|
||||||
|
}
|
||||||
|
provider, err := pgn.Lookup("Provider")
|
||||||
|
if err != nil {
|
||||||
|
return zero, fmt.Errorf("failed to lookup Provider for %s: %w", id, err)
|
||||||
|
}
|
||||||
|
log.Debugf(context.Background(), "Provider type: %T\n", provider)
|
||||||
|
|
||||||
|
pp, ok := provider.(T)
|
||||||
|
if !ok {
|
||||||
|
return zero, fmt.Errorf("failed to cast Provider for %s", id)
|
||||||
|
}
|
||||||
|
log.Debugf(context.Background(), "Casting successful for: %s", provider)
|
||||||
|
return pp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publisher returns a Publisher instance based on the provided configuration.
|
||||||
|
// It reuses the loaded provider and registers a cleanup function.
|
||||||
|
func (m *Manager) Publisher(ctx context.Context, cfg *Config) (definition.Publisher, error) {
|
||||||
|
pp, err := provider[definition.PublisherProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
p, closer, err := pp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaValidator returns a SchemaValidator instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) SchemaValidator(ctx context.Context, cfg *Config) (definition.SchemaValidator, error) {
|
||||||
|
vp, err := provider[definition.SchemaValidatorProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
v, closer, err := vp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router returns a Router instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) Router(ctx context.Context, cfg *Config) (definition.Router, error) {
|
||||||
|
rp, err := provider[definition.RouterProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
router, closer, err := rp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return router, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware returns an HTTP middleware function based on the provided configuration.
|
||||||
|
func (m *Manager) Middleware(ctx context.Context, cfg *Config) (func(http.Handler) http.Handler, error) {
|
||||||
|
mwp, err := provider[definition.MiddlewareProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
return mwp.New(ctx, cfg.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step returns a Step instance based on the provided configuration.
|
||||||
|
func (m *Manager) Step(ctx context.Context, cfg *Config) (definition.Step, error) {
|
||||||
|
sp, err := provider[definition.StepProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
step, closer, error := sp.New(ctx, cfg.Config)
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, closer)
|
||||||
|
}
|
||||||
|
return step, error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache returns a Cache instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) Cache(ctx context.Context, cfg *Config) (definition.Cache, error) {
|
||||||
|
cp, err := provider[definition.CacheProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
c, closer, err := cp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signer returns a Signer instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) Signer(ctx context.Context, cfg *Config) (definition.Signer, error) {
|
||||||
|
sp, err := provider[definition.SignerProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
s, closer, err := sp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encryptor returns an Encrypter instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) Encryptor(ctx context.Context, cfg *Config) (definition.Encrypter, error) {
|
||||||
|
ep, err := provider[definition.EncrypterProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
encrypter, closer, err := ep.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return encrypter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decryptor returns a Decrypter instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) Decryptor(ctx context.Context, cfg *Config) (definition.Decrypter, error) {
|
||||||
|
dp, err := provider[definition.DecrypterProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypter, closer, err := dp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignValidator returns a SignValidator instance based on the provided configuration.
|
||||||
|
// It registers a cleanup function for resource management.
|
||||||
|
func (m *Manager) SignValidator(ctx context.Context, cfg *Config) (definition.SignValidator, error) {
|
||||||
|
svp, err := provider[definition.SignValidatorProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
v, closer, err := svp.New(ctx, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyManager returns a KeyManager instance based on the provided configuration.
|
||||||
|
// It reuses the loaded provider.
|
||||||
|
func (m *Manager) KeyManager(ctx context.Context, cache definition.Cache, rClient definition.RegistryLookup, cfg *Config) (definition.KeyManager, error) {
|
||||||
|
|
||||||
|
kmp, err := provider[definition.KeyManagerProvider](m.plugins, cfg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err)
|
||||||
|
}
|
||||||
|
km, closer, err := kmp.New(ctx, cache, rClient, cfg.Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if closer != nil {
|
||||||
|
m.closers = append(m.closers, func() {
|
||||||
|
if err := closer(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return km, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator implements handler.PluginManager.
|
||||||
|
func (m *Manager) Validator(ctx context.Context, cfg *Config) (definition.SchemaValidator, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unzip extracts a ZIP file to the specified destination
|
||||||
|
func unzip(src, dest string) error {
|
||||||
|
r, err := zip.OpenReader(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
// Ensure the destination directory exists
|
||||||
|
if err := os.MkdirAll(dest, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
|
||||||
|
fpath := filepath.Join(dest, f.Name)
|
||||||
|
// Ensure directory exists
|
||||||
|
log.Debugf(context.Background(), "Pain : fpath: %s,filepath.Dir(fpath): %s", fpath, filepath.Dir(fpath))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Open the file inside the zip
|
||||||
|
srcFile, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// Create the destination file
|
||||||
|
dstFile, err := os.Create(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
// Copy file contents
|
||||||
|
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
>>>>>>> fdec61e90d57d3d82345d023c1a0d33d5a90583b
|
||||||
}
|
}
|
||||||
|
|||||||
2495
pkg/plugin/manager_test.go
Normal file
2495
pkg/plugin/manager_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,357 +0,0 @@
|
|||||||
{
|
|
||||||
"context": {
|
|
||||||
"action": "on_init",
|
|
||||||
"bap_id": "example-bap.com",
|
|
||||||
"bap_uri": "https://example-bap.com/prod/trv10",
|
|
||||||
"bpp_id": "example-bpp.com",
|
|
||||||
"bpp_uri": "https://example-bpp.com/prod/seller",
|
|
||||||
"domain": "ONDC:TRV10",
|
|
||||||
"location": {
|
|
||||||
"city": {
|
|
||||||
"code": "std:080"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"code": "IND"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"message_id": "8926b747-0362-4fcc-b795-0994a6287700",
|
|
||||||
"timestamp": "2023-12-09T14:11:32.859Z",
|
|
||||||
"transaction_id": "870782be-6757-43f1-945c-8eeaf9536259",
|
|
||||||
"ttl": "PT30S",
|
|
||||||
"version": "2.0.1"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"order": {
|
|
||||||
"cancellation_terms": [
|
|
||||||
{
|
|
||||||
"cancellation_fee": {
|
|
||||||
"percentage": "0"
|
|
||||||
},
|
|
||||||
"fulfillment_state": {
|
|
||||||
"descriptor": {
|
|
||||||
"code": "RIDE_ASSIGNED"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reason_required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cancellation_fee": {
|
|
||||||
"amount": {
|
|
||||||
"currency": "INR",
|
|
||||||
"value": "30"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fulfillment_state": {
|
|
||||||
"descriptor": {
|
|
||||||
"code": "RIDE_ENROUTE_PICKUP"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reason_required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cancellation_fee": {
|
|
||||||
"amount": {
|
|
||||||
"currency": "INR",
|
|
||||||
"value": "50"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fulfillment_state": {
|
|
||||||
"descriptor": {
|
|
||||||
"code": "RIDE_ARRIVED_PICKUP"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reason_required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cancellation_fee": {
|
|
||||||
"percentage": "100"
|
|
||||||
},
|
|
||||||
"fulfillment_state": {
|
|
||||||
"descriptor": {
|
|
||||||
"code": "RIDE_STARTED"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reason_required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fulfillments": [
|
|
||||||
{
|
|
||||||
"id": "F1",
|
|
||||||
"customer": {
|
|
||||||
"contact": {
|
|
||||||
"phone": "9876556789"
|
|
||||||
},
|
|
||||||
"person": {
|
|
||||||
"name": "Joe Adams"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stops": [
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "13.008935, 77.644408"
|
|
||||||
},
|
|
||||||
"type": "START",
|
|
||||||
"instructions": {
|
|
||||||
"short_desc": "short description of the location",
|
|
||||||
"long_desc": "long description of the location"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "12.971186, 77.586812"
|
|
||||||
},
|
|
||||||
"type": "END"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "ROUTE_INFO",
|
|
||||||
"name": "Route Information"
|
|
||||||
},
|
|
||||||
"display": true,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "ENCODED_POLYLINE",
|
|
||||||
"name": "Path"
|
|
||||||
},
|
|
||||||
"value": "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "WAYPOINTS",
|
|
||||||
"name": "Waypoints"
|
|
||||||
},
|
|
||||||
"value": "[{\"gps\":\"12.909982, 77.611822\"},{\"gps\":\"12.909982,77.611822\"},{\"gps\":\"12.909982,77.611822\"},{\"gps\":\"12.909982, 77.611822\"}]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "DELIVERY",
|
|
||||||
"vehicle": {
|
|
||||||
"category": "AUTO_RICKSHAW",
|
|
||||||
"variant": "EV"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "RIDE",
|
|
||||||
"name": "Auto Ride"
|
|
||||||
},
|
|
||||||
"fulfillment_ids": [
|
|
||||||
"F1"
|
|
||||||
],
|
|
||||||
"id": "I1",
|
|
||||||
"location_ids": [
|
|
||||||
"L1",
|
|
||||||
"L3"
|
|
||||||
],
|
|
||||||
"payment_ids": [
|
|
||||||
"PA1"
|
|
||||||
],
|
|
||||||
"price": {
|
|
||||||
"currency": "INR",
|
|
||||||
"maximum_value": "176",
|
|
||||||
"minimum_value": "136",
|
|
||||||
"value": "146"
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "FARE_POLICY",
|
|
||||||
"name": "Daytime Charges"
|
|
||||||
},
|
|
||||||
"display": true,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "MIN_FARE"
|
|
||||||
},
|
|
||||||
"value": "30"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "MIN_FARE_DISTANCE_KM"
|
|
||||||
},
|
|
||||||
"value": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "PER_KM_CHARGE"
|
|
||||||
},
|
|
||||||
"value": "15"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "PICKUP_CHARGE"
|
|
||||||
},
|
|
||||||
"value": "10"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "WAITING_CHARGE_PER_MIN"
|
|
||||||
},
|
|
||||||
"value": "2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "NIGHT_CHARGE_MULTIPLIER"
|
|
||||||
},
|
|
||||||
"value": "1.5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "NIGHT_SHIFT_START_TIME"
|
|
||||||
},
|
|
||||||
"value": "22:00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "NIGHT_SHIFT_END_TIME"
|
|
||||||
},
|
|
||||||
"value": "05:00:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "INFO",
|
|
||||||
"name": "General Information"
|
|
||||||
},
|
|
||||||
"display": true,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "DISTANCE_TO_NEAREST_DRIVER_METER"
|
|
||||||
},
|
|
||||||
"value": "661"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "ETA_TO_NEAREST_DRIVER_MIN"
|
|
||||||
},
|
|
||||||
"value": "3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"payments": [
|
|
||||||
{
|
|
||||||
"collected_by": "BPP",
|
|
||||||
"id": "PA1",
|
|
||||||
"params": {
|
|
||||||
"bank_account_number": "xxxxxxxxxxxxxx",
|
|
||||||
"bank_code": "XXXXXXXX",
|
|
||||||
"virtual_payment_address": "9988199772@okicic"
|
|
||||||
},
|
|
||||||
"status": "NOT-PAID",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES_PERCENTAGE"
|
|
||||||
},
|
|
||||||
"value": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_TERMS"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "DELAY_INTEREST"
|
|
||||||
},
|
|
||||||
"value": "5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_TYPE"
|
|
||||||
},
|
|
||||||
"value": "UPI"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_WINDOW"
|
|
||||||
},
|
|
||||||
"value": "PT2D"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_BASIS"
|
|
||||||
},
|
|
||||||
"value": "DELIVERY"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "MANDATORY_ARBITRATION"
|
|
||||||
},
|
|
||||||
"value": "true"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "COURT_JURISDICTION"
|
|
||||||
},
|
|
||||||
"value": "New Delhi"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "STATIC_TERMS"
|
|
||||||
},
|
|
||||||
"value": "https://example-test-bpp.com/static-terms.txt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_AMOUNT"
|
|
||||||
},
|
|
||||||
"value": "1.46"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "ON-FULFILLMENT"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"provider": {
|
|
||||||
"id": "P1"
|
|
||||||
},
|
|
||||||
"quote": {
|
|
||||||
"breakup": [
|
|
||||||
{
|
|
||||||
"price": {
|
|
||||||
"currency": "INR",
|
|
||||||
"value": "30"
|
|
||||||
},
|
|
||||||
"title": "BASE_FARE"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"price": {
|
|
||||||
"currency": "INR",
|
|
||||||
"value": "116"
|
|
||||||
},
|
|
||||||
"title": "DISTANCE_FARE"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"price": {
|
|
||||||
"currency": "INR",
|
|
||||||
"value": "146"
|
|
||||||
},
|
|
||||||
"ttl": "PT30S"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
{
|
|
||||||
"context": {
|
|
||||||
"action": "search",
|
|
||||||
"bap_id": "example-bap.com",
|
|
||||||
"bap_uri": "https://example-bap.com/prod/trv10",
|
|
||||||
"domain": "ONDC:TRV10",
|
|
||||||
"location": {
|
|
||||||
"city": {
|
|
||||||
"code": "std:080"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"code": "IND"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"message_id": "40963dc1-e402-4f4d-ae70-7c5864ca682c",
|
|
||||||
"timestamp": "2023-12-09T13:39:56.645Z",
|
|
||||||
"transaction_id": "870782be-6757-43f1-945c-8eeaf9536259",
|
|
||||||
"ttl": "PT30S",
|
|
||||||
"version": "2.0.1"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"intent": {
|
|
||||||
"fulfillment": {
|
|
||||||
"stops": [
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "13.008935, 77.644408"
|
|
||||||
},
|
|
||||||
"type": "START"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "12.971186, 77.586812"
|
|
||||||
},
|
|
||||||
"type": "END"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payment": {
|
|
||||||
"collected_by": "BPP",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES_PERCENTAGE"
|
|
||||||
},
|
|
||||||
"value": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_TERMS"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "DELAY_INTEREST"
|
|
||||||
},
|
|
||||||
"value": "5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "STATIC_TERMS"
|
|
||||||
},
|
|
||||||
"value": "https://example-test-bap.com/static-terms.txt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
{
|
|
||||||
"context": {
|
|
||||||
"action": "search",
|
|
||||||
"bap_id": "example-bap.com",
|
|
||||||
"bap_uri": "https://example-bap.com/prod/trv10",
|
|
||||||
"domain": "ONDC:TRV10",
|
|
||||||
"location": {
|
|
||||||
"city": {
|
|
||||||
"code": "std:080"
|
|
||||||
},
|
|
||||||
"country": {
|
|
||||||
"code": "IND"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"message_id": "40963dc1-e402-4f4d-ae70-7c5864ca682c",
|
|
||||||
"timestamp": "2023-12-09T13:40:21.452Z",
|
|
||||||
"transaction_id": "870782be-6757-43f1-945c-8eeaf9536259",
|
|
||||||
"ttl": "PT30S",
|
|
||||||
"version": "2.0.1"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"intent": {
|
|
||||||
"fulfillment": {
|
|
||||||
"stops": [
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "13.008935, 77.644408"
|
|
||||||
},
|
|
||||||
"type": "START"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"location": {
|
|
||||||
"gps": "12.971186, 77.586812"
|
|
||||||
},
|
|
||||||
"type": "END"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"payment": {
|
|
||||||
"collected_by": "BAP",
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "BUYER_FINDER_FEES_PERCENTAGE"
|
|
||||||
},
|
|
||||||
"value": "1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_TERMS"
|
|
||||||
},
|
|
||||||
"display": false,
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_WINDOW"
|
|
||||||
},
|
|
||||||
"value": "PT1D"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "SETTLEMENT_BASIS"
|
|
||||||
},
|
|
||||||
"value": "DELIVERY"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"descriptor": {
|
|
||||||
"code": "STATIC_TERMS"
|
|
||||||
},
|
|
||||||
"value": "https://example-test-bap.com/static-terms.txt"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
pkg/plugin/testdata/dummy.go
vendored
Normal file
18
pkg/plugin/testdata/dummy.go
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/plugin/implementation/encrypter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// encrypterProvider implements the definition.encrypterProvider interface.
|
||||||
|
type encrypterProvider struct{}
|
||||||
|
|
||||||
|
func (ep encrypterProvider) New(ctx context.Context, config map[string]string) (definition.Encrypter, func() error, error) {
|
||||||
|
return encrypter.New(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider is the exported symbol that the plugin manager will look for.
|
||||||
|
var Provider = encrypterProvider{}
|
||||||
@@ -3,150 +3,87 @@ package response
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/log"
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrorType string
|
// SendAck sends an acknowledgment response (ACK) to the client.
|
||||||
|
func SendAck(w http.ResponseWriter) {
|
||||||
const (
|
resp := &model.Response{
|
||||||
SchemaValidationErrorType ErrorType = "SCHEMA_VALIDATION_ERROR"
|
Message: model.Message{
|
||||||
InvalidRequestErrorType ErrorType = "INVALID_REQUEST"
|
Ack: model.Ack{
|
||||||
)
|
Status: model.StatusACK,
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SchemaValidationErr represents a collection of schema validation failures.
|
|
||||||
type SchemaValidationErr struct {
|
|
||||||
Errors []Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the error interface for SchemaValidationErr.
|
|
||||||
func (e *SchemaValidationErr) Error() string {
|
|
||||||
var errorMessages []string
|
|
||||||
for _, err := range e.Errors {
|
|
||||||
errorMessages = append(errorMessages, fmt.Sprintf("%s: %s", err.Paths, err.Message))
|
|
||||||
}
|
|
||||||
return strings.Join(errorMessages, "; ")
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ack processes the incoming Beckn request, unmarshals the JSON body into a BecknRequest struct,
|
|
||||||
// and returns a JSON-encoded acknowledgment response with a status of "ACK".
|
|
||||||
// If the request body cannot be parsed, it returns an error.
|
|
||||||
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)
|
|
||||||
|
data, _ := json.Marshal(resp) //should not fail here
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err := w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to write response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleClientFailure processes a client failure scenario by unmarshaling the provided
|
// nack sends a negative acknowledgment (NACK) response with an error message.
|
||||||
// request body, determining the appropriate error response based on the given ErrorType,
|
func nack(ctx context.Context, w http.ResponseWriter, err *model.Error, status int) {
|
||||||
// and returning the serialized response. If the ErrorType is not found in the error map,
|
resp := &model.Response{
|
||||||
// a default error is used.
|
Message: model.Message{
|
||||||
func HandleClientFailure(ctx context.Context, tp ErrorType, body []byte) ([]byte, error) {
|
Ack: model.Ack{
|
||||||
var req BecknRequest
|
Status: model.StatusNACK,
|
||||||
if err := json.Unmarshal(body, &req); err != nil {
|
},
|
||||||
return nil, fmt.Errorf("failed to parse request: %w", err)
|
Error: err,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
errorObj, ok := errorMap[tp]
|
data, _ := json.Marshal(resp) //should not fail here
|
||||||
var response ClientFailureBecknResponse
|
|
||||||
if !ok {
|
w.Header().Set("Content-Type", "application/json")
|
||||||
response = ClientFailureBecknResponse{
|
w.WriteHeader(status)
|
||||||
Context: req.Context,
|
_, er := w.Write(data)
|
||||||
Error: &DefaultError,
|
if er != nil {
|
||||||
}
|
log.Debugf(ctx, "Error writing response: %v, MessageID: %s", er, ctx.Value(model.ContextKeyMsgID))
|
||||||
} else {
|
http.Error(w, fmt.Sprintf("Internal server error, MessageID: %s", ctx.Value(model.ContextKeyMsgID)), http.StatusInternalServerError)
|
||||||
response = ClientFailureBecknResponse{
|
return
|
||||||
Context: req.Context,
|
}
|
||||||
Error: &errorObj,
|
}
|
||||||
}
|
|
||||||
|
// internalServerError generates an internal server error response.
|
||||||
|
func internalServerError(ctx context.Context) *model.Error {
|
||||||
|
return &model.Error{
|
||||||
|
Code: http.StatusText(http.StatusInternalServerError),
|
||||||
|
Message: fmt.Sprintf("Internal server error, MessageID: %s", ctx.Value(model.ContextKeyMsgID)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendNack processes different types of errors and sends an appropriate NACK response.
|
||||||
|
func SendNack(ctx context.Context, w http.ResponseWriter, err error) {
|
||||||
|
var schemaErr *model.SchemaValidationErr
|
||||||
|
var signErr *model.SignValidationErr
|
||||||
|
var badReqErr *model.BadReqErr
|
||||||
|
var notFoundErr *model.NotFoundErr
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &schemaErr):
|
||||||
|
nack(ctx, w, schemaErr.BecknError(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
case errors.As(err, &signErr):
|
||||||
|
nack(ctx, w, signErr.BecknError(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
case errors.As(err, &badReqErr):
|
||||||
|
nack(ctx, w, badReqErr.BecknError(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
case errors.As(err, ¬FoundErr):
|
||||||
|
nack(ctx, w, notFoundErr.BecknError(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
nack(ctx, w, internalServerError(ctx), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return json.Marshal(response)
|
|
||||||
}
|
}
|
||||||
|
|||||||
241
pkg/response/response_test.go
Normal file
241
pkg/response/response_test.go
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/beckn/beckn-onix/pkg/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errorResponseWriter struct{}
|
||||||
|
|
||||||
|
// TODO: Optimize the cases by removing these
|
||||||
|
func (e *errorResponseWriter) Write([]byte) (int, error) {
|
||||||
|
return 0, errors.New("write error")
|
||||||
|
}
|
||||||
|
func (e *errorResponseWriter) WriteHeader(statusCode int) {}
|
||||||
|
|
||||||
|
func (e *errorResponseWriter) Header() http.Header {
|
||||||
|
return http.Header{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendAck(t *testing.T) {
|
||||||
|
_, err := http.NewRequest("GET", "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err) // For tests
|
||||||
|
}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
SendAck(rr)
|
||||||
|
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Errorf("wanted status code %d, got %d", http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := `{"message":{"ack":{"status":"ACK"}}}`
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
rr.Body.String(), expected)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendNack(t *testing.T) {
|
||||||
|
ctx := context.WithValue(context.Background(), model.ContextKeyMsgID, "123456")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expected string
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SchemaValidationErr",
|
||||||
|
err: &model.SchemaValidationErr{
|
||||||
|
Errors: []model.Error{
|
||||||
|
{Paths: "/path1", Message: "Error 1"},
|
||||||
|
{Paths: "/path2", Message: "Error 2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"Bad Request","paths":"/path1;/path2","message":"Error 1; Error 2"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SignValidationErr",
|
||||||
|
err: model.NewSignValidationErr(errors.New("signature invalid")),
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"Unauthorized","message":"Signature Validation Error: signature invalid"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BadReqErr",
|
||||||
|
err: model.NewBadReqErr(errors.New("bad request error")),
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"Bad Request","message":"BAD Request: bad request error"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotFoundErr",
|
||||||
|
err: model.NewNotFoundErr(errors.New("endpoint not found")),
|
||||||
|
status: http.StatusNotFound,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"Not Found","message":"Endpoint not found: endpoint not found"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "InternalServerError",
|
||||||
|
err: errors.New("unexpected error"),
|
||||||
|
status: http.StatusInternalServerError,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"Internal Server Error","message":"Internal server error, MessageID: 123456"}}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := http.NewRequest("GET", "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err) // For tests
|
||||||
|
}
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
SendNack(ctx, rr, tt.err)
|
||||||
|
|
||||||
|
if rr.Code != tt.status {
|
||||||
|
t.Errorf("wanted status code %d, got %d", tt.status, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual map[string]interface{}
|
||||||
|
err = json.Unmarshal(rr.Body.Bytes(), &actual)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(tt.expected), &expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to unmarshal expected response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compareJSON(expected, actual) {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareJSON(expected, actual map[string]interface{}) bool {
|
||||||
|
expectedBytes, _ := json.Marshal(expected)
|
||||||
|
actualBytes, _ := json.Marshal(actual)
|
||||||
|
return bytes.Equal(expectedBytes, actualBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendAck_WriteError(t *testing.T) {
|
||||||
|
w := &errorResponseWriter{}
|
||||||
|
SendAck(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock struct to force JSON marshalling error
|
||||||
|
type badMessage struct{}
|
||||||
|
|
||||||
|
func (b *badMessage) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("marshal error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNack_1(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err *model.Error
|
||||||
|
status int
|
||||||
|
expected string
|
||||||
|
useBadJSON bool
|
||||||
|
useBadWrite bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Schema Validation Error",
|
||||||
|
err: &model.Error{
|
||||||
|
Code: "BAD_REQUEST",
|
||||||
|
Paths: "/test/path",
|
||||||
|
Message: "Invalid schema",
|
||||||
|
},
|
||||||
|
status: http.StatusBadRequest,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"BAD_REQUEST","paths":"/test/path","message":"Invalid schema"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Internal Server Error",
|
||||||
|
err: &model.Error{
|
||||||
|
Code: "INTERNAL_SERVER_ERROR",
|
||||||
|
Message: "Something went wrong",
|
||||||
|
},
|
||||||
|
status: http.StatusInternalServerError,
|
||||||
|
expected: `{"message":{"ack":{"status":"NACK"},"error":{"code":"INTERNAL_SERVER_ERROR","message":"Something went wrong"}}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "JSON Marshal Error",
|
||||||
|
err: nil, // This will be overridden to cause marshaling error
|
||||||
|
status: http.StatusInternalServerError,
|
||||||
|
expected: `Internal server error, MessageID: 12345`,
|
||||||
|
useBadJSON: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Write Error",
|
||||||
|
err: &model.Error{
|
||||||
|
Code: "WRITE_ERROR",
|
||||||
|
Message: "Failed to write response",
|
||||||
|
},
|
||||||
|
status: http.StatusInternalServerError,
|
||||||
|
expected: `Internal server error, MessageID: 12345`,
|
||||||
|
useBadWrite: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequest("GET", "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(req.Context(), model.ContextKeyMsgID, "12345")
|
||||||
|
|
||||||
|
var w http.ResponseWriter
|
||||||
|
if tt.useBadWrite {
|
||||||
|
w = &errorResponseWriter{} // Simulate write error
|
||||||
|
} else {
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix this approach , should not be used like this.
|
||||||
|
if tt.useBadJSON {
|
||||||
|
data, _ := json.Marshal(&badMessage{})
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(tt.status)
|
||||||
|
_, err := w.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to write response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nack(ctx, w, tt.err, tt.status)
|
||||||
|
if !tt.useBadWrite {
|
||||||
|
recorder, ok := w.(*httptest.ResponseRecorder)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Failed to cast response recorder")
|
||||||
|
}
|
||||||
|
|
||||||
|
if recorder.Code != tt.status {
|
||||||
|
t.Errorf("wanted status code %d, got %d", tt.status, recorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := recorder.Body.String()
|
||||||
|
if body != tt.expected {
|
||||||
|
t.Errorf("err.Error() = %s, want %s",
|
||||||
|
body, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
schemas.zip
Normal file
BIN
schemas.zip
Normal file
Binary file not shown.
92
test.go
92
test.go
@@ -1,92 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
|
||||||
|
|
||||||
"github.com/beckn/beckn-onix/pkg/plugin"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
manager *plugin.Manager
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var err error
|
|
||||||
// Load the configuration.
|
|
||||||
config, err := plugin.LoadConfig("pkg/plugin/plugin.yaml")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to load plugins configuration: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the plugin manager.
|
|
||||||
manager, err = plugin.NewManager(context.Background(), config)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create PluginManager: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the validator.
|
|
||||||
validator, _, defErr := manager.SchemaValidator(context.Background())
|
|
||||||
if defErr != nil {
|
|
||||||
log.Fatalf("Failed to get validators: %v", defErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
validateHandler(w, r, validator)
|
|
||||||
})
|
|
||||||
fmt.Println("Starting server on port 8084...")
|
|
||||||
err = http.ListenAndServe(":8084", nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Server failed to start: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHandler(w http.ResponseWriter, r *http.Request, validators definition.SchemaValidator) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract endpoint from request URL.
|
|
||||||
requestURL := r.RequestURI
|
|
||||||
u, err := url.ParseRequestURI(requestURL)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to parse request URL", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payloadData, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to read payload data", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
// validationErr := validators.Validate(ctx, u, payloadData)
|
|
||||||
// if validationErr != (definition.SchemaValError{}) {
|
|
||||||
// http.Error(w, fmt.Sprintf("Document validation failed: %v", validationErr), http.StatusBadRequest)
|
|
||||||
// } else if !valid {
|
|
||||||
// http.Error(w, "Document validation failed", http.StatusBadRequest)
|
|
||||||
// } else {
|
|
||||||
// w.WriteHeader(http.StatusOK)
|
|
||||||
// if _, err := w.Write([]byte("Document validation succeeded!")); err != nil {
|
|
||||||
// log.Fatalf("Failed to write response: %v", err)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
validationErr := validators.Validate(ctx, u, payloadData)
|
|
||||||
if validationErr != nil {
|
|
||||||
// Handle other types of errors
|
|
||||||
http.Error(w, fmt.Sprintf("Schema validation failed: %v", validationErr), http.StatusBadRequest)
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
if _, err := w.Write([]byte("Schema validation succeeded!")); err != nil {
|
|
||||||
log.Fatalf("Failed to write response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user