- Add transactionId and messageId as camelCase aliases in model.go contextKeys map, pointing to the same ContextKeyTxnID and ContextKeyMsgID constants. This allows adapter configs to reference either form without failing startup validation. - In reqpreprocessor, add firstNonNil() helper for subscriber/caller ID lookups so bap_id/bapId and bpp_id/bppId are both resolved correctly regardless of which beckn spec version the payload uses. snake_case takes precedence when both are present. - Add snakeToCamel() helper used in the context key loop so a single config entry (e.g. transaction_id) automatically also checks the camelCase form (transactionId) without requiring any config file changes. - Add TestSnakeToCamel, TestCamelCaseSubscriberID, TestCamelCaseContextKeys to cover all new code paths. Fixes #637 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
410 lines
11 KiB
Go
410 lines
11 KiB
Go
package reqpreprocessor
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/beckn-one/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",
|
|
ParentID: "bap:bap-123",
|
|
},
|
|
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",
|
|
ParentID: "bap:bap-123",
|
|
},
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSnakeToCamel tests the snakeToCamel conversion helper.
|
|
func TestSnakeToCamel(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
want string
|
|
}{
|
|
{"transaction_id", "transactionId"},
|
|
{"message_id", "messageId"},
|
|
{"bap_id", "bapId"},
|
|
{"bpp_id", "bppId"},
|
|
{"bap_uri", "bapUri"},
|
|
{"bpp_uri", "bppUri"},
|
|
{"domain", "domain"}, // no underscore — unchanged
|
|
{"version", "version"}, // no underscore — unchanged
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got := snakeToCamel(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("snakeToCamel(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCamelCaseSubscriberID tests that bapId / bppId are resolved when the payload
|
|
// uses camelCase context attribute names (new beckn spec).
|
|
func TestCamelCaseSubscriberID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
role string
|
|
contextBody map[string]interface{}
|
|
wantSubID string
|
|
wantCaller string
|
|
}{
|
|
{
|
|
name: "BAP role — camelCase bapId resolved as subscriber",
|
|
role: "bap",
|
|
contextBody: map[string]interface{}{
|
|
"bapId": "bap.example.com",
|
|
"bppId": "bpp.example.com",
|
|
},
|
|
wantSubID: "bap.example.com",
|
|
wantCaller: "bpp.example.com",
|
|
},
|
|
{
|
|
name: "BPP role — camelCase bppId resolved as subscriber",
|
|
role: "bpp",
|
|
contextBody: map[string]interface{}{
|
|
"bapId": "bap.example.com",
|
|
"bppId": "bpp.example.com",
|
|
},
|
|
wantSubID: "bpp.example.com",
|
|
wantCaller: "bap.example.com",
|
|
},
|
|
{
|
|
name: "snake_case still takes precedence over camelCase",
|
|
role: "bap",
|
|
contextBody: map[string]interface{}{
|
|
"bap_id": "bap-snake.example.com",
|
|
"bapId": "bap-camel.example.com",
|
|
"bpp_id": "bpp-snake.example.com",
|
|
"bppId": "bpp-camel.example.com",
|
|
},
|
|
wantSubID: "bap-snake.example.com",
|
|
wantCaller: "bpp-snake.example.com",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := &Config{Role: tt.role}
|
|
middleware, err := NewPreProcessor(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewPreProcessor() error = %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{"context": tt.contextBody})
|
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
|
|
|
var gotSubID, gotCaller interface{}
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotSubID = r.Context().Value(model.ContextKeySubscriberID)
|
|
gotCaller = r.Context().Value(model.ContextKeyRemoteID)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
middleware(handler).ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
if gotSubID != tt.wantSubID {
|
|
t.Errorf("subscriber ID: got %v, want %v", gotSubID, tt.wantSubID)
|
|
}
|
|
if gotCaller != tt.wantCaller {
|
|
t.Errorf("caller ID: got %v, want %v", gotCaller, tt.wantCaller)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCamelCaseContextKeys tests that generic context keys (e.g. transaction_id)
|
|
// are resolved from their camelCase equivalents (transactionId) when the
|
|
// snake_case key is absent from the payload.
|
|
func TestCamelCaseContextKeys(t *testing.T) {
|
|
cfg := &Config{
|
|
Role: "bap",
|
|
ContextKeys: []string{"transaction_id", "message_id"},
|
|
}
|
|
middleware, err := NewPreProcessor(cfg)
|
|
if err != nil {
|
|
t.Fatalf("NewPreProcessor() error = %v", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"context": map[string]interface{}{
|
|
"bapId": "bap.example.com",
|
|
"transactionId": "txn-abc",
|
|
"messageId": "msg-xyz",
|
|
},
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body))
|
|
|
|
var gotTxnID, gotMsgID interface{}
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotTxnID = r.Context().Value(model.ContextKeyTxnID)
|
|
gotMsgID = r.Context().Value(model.ContextKeyMsgID)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
middleware(handler).ServeHTTP(httptest.NewRecorder(), req)
|
|
|
|
if gotTxnID != "txn-abc" {
|
|
t.Errorf("transaction_id: got %v, want txn-abc", gotTxnID)
|
|
}
|
|
if gotMsgID != "msg-xyz" {
|
|
t.Errorf("message_id: got %v, want msg-xyz", gotMsgID)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|