solve schema validation conflicts

This commit is contained in:
AshwiniK-protean
2025-04-23 18:28:47 +05:30
93 changed files with 11199 additions and 758 deletions

View 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{}

View 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)
}
}
})
}
}

View 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
}

View 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")
}
}
})
}
}

View 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{}

View 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)
}
}
}()
})
}
}

View 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
}

View 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")
}
})
}
}

View 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{}

View 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)
})
}
}

View 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
}

View File

@@ -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)
}
}

View 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{}

View 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)
}
})
}
}

View 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
}

View 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)
}
})
}
}

View 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

View 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

View 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

View 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

View 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{}

View 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
}
})
}
}

View 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
}

View File

@@ -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)
}
})
}
}

View 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{}

View 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)
}
})
}
}

View 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
}

View 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)
}
}
})
}
}

View 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{}

View 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)
}
}
})
}
}

View 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)
}

View 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)
}
}
})
}
}