This is page 5 of 5. Use http://codebase.md/razorpay/razorpay-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .cursor │ └── rules │ └── new-tool-from-docs.mdc ├── .cursorignore ├── .dockerignore ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows │ ├── assign.yml │ ├── build.yml │ ├── ci.yml │ ├── docker-publish.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── cmd │ └── razorpay-mcp-server │ ├── main.go │ └── stdio.go ├── codecov.yml ├── CONTRIBUTING.md ├── Dockerfile ├── go.mod ├── go.sum ├── LICENSE ├── Makefile ├── pkg │ ├── contextkey │ │ └── context_key.go │ ├── log │ │ ├── config.go │ │ ├── log.go │ │ ├── slog_test.go │ │ └── slog.go │ ├── mcpgo │ │ ├── README.md │ │ ├── server.go │ │ ├── stdio.go │ │ ├── tool.go │ │ └── transport.go │ ├── observability │ │ └── observability.go │ ├── razorpay │ │ ├── mock │ │ │ ├── server_test.go │ │ │ └── server.go │ │ ├── orders_test.go │ │ ├── orders.go │ │ ├── payment_links_test.go │ │ ├── payment_links.go │ │ ├── payments_test.go │ │ ├── payments.go │ │ ├── payouts_test.go │ │ ├── payouts.go │ │ ├── qr_codes_test.go │ │ ├── qr_codes.go │ │ ├── README.md │ │ ├── refunds_test.go │ │ ├── refunds.go │ │ ├── server.go │ │ ├── settlements_test.go │ │ ├── settlements.go │ │ ├── test_helpers.go │ │ ├── tokens_test.go │ │ ├── tokens.go │ │ ├── tools_params_test.go │ │ ├── tools_params.go │ │ ├── tools_test.go │ │ └── tools.go │ └── toolsets │ └── toolsets.go ├── README.md └── SECURITY.md ``` # Files -------------------------------------------------------------------------------- /pkg/razorpay/payments_test.go: -------------------------------------------------------------------------------- ```go 1 | package razorpay 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/razorpay/razorpay-go/constants" 12 | 13 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo" 14 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock" 15 | ) 16 | 17 | func Test_FetchPayment(t *testing.T) { 18 | fetchPaymentPathFmt := fmt.Sprintf( 19 | "/%s%s/%%s", 20 | constants.VERSION_V1, 21 | constants.PAYMENT_URL, 22 | ) 23 | 24 | paymentResp := map[string]interface{}{ 25 | "id": "pay_MT48CvBhIC98MQ", 26 | "amount": float64(1000), 27 | "status": "captured", 28 | } 29 | 30 | paymentNotFoundResp := map[string]interface{}{ 31 | "error": map[string]interface{}{ 32 | "code": "BAD_REQUEST_ERROR", 33 | "description": "payment not found", 34 | }, 35 | } 36 | 37 | tests := []RazorpayToolTestCase{ 38 | { 39 | Name: "successful payment fetch", 40 | Request: map[string]interface{}{ 41 | "payment_id": "pay_MT48CvBhIC98MQ", 42 | }, 43 | MockHttpClient: func() (*http.Client, *httptest.Server) { 44 | return mock.NewHTTPClient( 45 | mock.Endpoint{ 46 | Path: fmt.Sprintf(fetchPaymentPathFmt, "pay_MT48CvBhIC98MQ"), 47 | Method: "GET", 48 | Response: paymentResp, 49 | }, 50 | ) 51 | }, 52 | ExpectError: false, 53 | ExpectedResult: paymentResp, 54 | }, 55 | { 56 | Name: "payment not found", 57 | Request: map[string]interface{}{ 58 | "payment_id": "pay_invalid", 59 | }, 60 | MockHttpClient: func() (*http.Client, *httptest.Server) { 61 | return mock.NewHTTPClient( 62 | mock.Endpoint{ 63 | Path: fmt.Sprintf(fetchPaymentPathFmt, "pay_invalid"), 64 | Method: "GET", 65 | Response: paymentNotFoundResp, 66 | }, 67 | ) 68 | }, 69 | ExpectError: true, 70 | ExpectedErrMsg: "fetching payment failed: payment not found", 71 | }, 72 | { 73 | Name: "missing payment_id parameter", 74 | Request: map[string]interface{}{}, 75 | MockHttpClient: nil, // No HTTP client needed for validation error 76 | ExpectError: true, 77 | ExpectedErrMsg: "missing required parameter: payment_id", 78 | }, 79 | } 80 | 81 | for _, tc := range tests { 82 | t.Run(tc.Name, func(t *testing.T) { 83 | runToolTest(t, tc, FetchPayment, "Payment") 84 | }) 85 | } 86 | } 87 | 88 | func Test_FetchPaymentCardDetails(t *testing.T) { 89 | fetchCardDetailsPathFmt := fmt.Sprintf( 90 | "/%s%s/%%s/card", 91 | constants.VERSION_V1, 92 | constants.PAYMENT_URL, 93 | ) 94 | 95 | cardDetailsResp := map[string]interface{}{ 96 | "id": "card_JXPULjlKqC5j0i", 97 | "entity": "card", 98 | "name": "Gaurav Kumar", 99 | "last4": "4366", 100 | "network": "Visa", 101 | "type": "credit", 102 | "issuer": "UTIB", 103 | "international": false, 104 | "emi": false, 105 | "sub_type": "consumer", 106 | "token_iin": nil, 107 | } 108 | 109 | paymentNotFoundResp := map[string]interface{}{ 110 | "error": map[string]interface{}{ 111 | "code": "BAD_REQUEST_ERROR", 112 | "description": "The id provided does not exist", 113 | }, 114 | } 115 | 116 | tests := []RazorpayToolTestCase{ 117 | { 118 | Name: "successful card details fetch", 119 | Request: map[string]interface{}{ 120 | "payment_id": "pay_DtFYPi3IfUTgsL", 121 | }, 122 | MockHttpClient: func() (*http.Client, *httptest.Server) { 123 | return mock.NewHTTPClient( 124 | mock.Endpoint{ 125 | Path: fmt.Sprintf(fetchCardDetailsPathFmt, "pay_DtFYPi3IfUTgsL"), 126 | Method: "GET", 127 | Response: cardDetailsResp, 128 | }, 129 | ) 130 | }, 131 | ExpectError: false, 132 | ExpectedResult: cardDetailsResp, 133 | }, 134 | { 135 | Name: "payment not found", 136 | Request: map[string]interface{}{ 137 | "payment_id": "pay_invalid", 138 | }, 139 | MockHttpClient: func() (*http.Client, *httptest.Server) { 140 | return mock.NewHTTPClient( 141 | mock.Endpoint{ 142 | Path: fmt.Sprintf(fetchCardDetailsPathFmt, "pay_invalid"), 143 | Method: "GET", 144 | Response: paymentNotFoundResp, 145 | }, 146 | ) 147 | }, 148 | ExpectError: true, 149 | ExpectedErrMsg: "fetching card details failed: " + 150 | "The id provided does not exist", 151 | }, 152 | { 153 | Name: "missing payment_id parameter", 154 | Request: map[string]interface{}{}, 155 | MockHttpClient: nil, // No HTTP client needed for validation error 156 | ExpectError: true, 157 | ExpectedErrMsg: "missing required parameter: payment_id", 158 | }, 159 | } 160 | 161 | for _, tc := range tests { 162 | t.Run(tc.Name, func(t *testing.T) { 163 | runToolTest(t, tc, FetchPaymentCardDetails, "Card Details") 164 | }) 165 | } 166 | } 167 | 168 | func Test_CapturePayment(t *testing.T) { 169 | capturePaymentPathFmt := fmt.Sprintf( 170 | "/%s%s/%%s/capture", 171 | constants.VERSION_V1, 172 | constants.PAYMENT_URL, 173 | ) 174 | 175 | successfulCaptureResp := map[string]interface{}{ 176 | "id": "pay_G3P9vcIhRs3NV4", 177 | "entity": "payment", 178 | "amount": float64(1000), 179 | "currency": "INR", 180 | "status": "captured", 181 | "order_id": "order_GjCr5oKh4AVC51", 182 | "invoice_id": nil, 183 | "international": false, 184 | "method": "card", 185 | "amount_refunded": float64(0), 186 | "refund_status": nil, 187 | "captured": true, 188 | "description": "Payment for Adidas shoes", 189 | "card_id": "card_KOdY30ajbuyOYN", 190 | "bank": nil, 191 | "wallet": nil, 192 | "vpa": nil, 193 | "email": "[email protected]", 194 | "contact": "9000090000", 195 | "customer_id": "cust_K6fNE0WJZWGqtN", 196 | "token_id": "token_KOdY$DBYQOv08n", 197 | "notes": []interface{}{}, 198 | "fee": float64(1), 199 | "tax": float64(0), 200 | "error_code": nil, 201 | "error_description": nil, 202 | "error_source": nil, 203 | "error_step": nil, 204 | "error_reason": nil, 205 | "acquirer_data": map[string]interface{}{ 206 | "authentication_reference_number": "100222021120200000000742753928", 207 | }, 208 | "created_at": float64(1605871409), 209 | } 210 | 211 | alreadyCapturedResp := map[string]interface{}{ 212 | "error": map[string]interface{}{ 213 | "code": "BAD_REQUEST_ERROR", 214 | "description": "This payment has already been captured", 215 | }, 216 | } 217 | 218 | tests := []RazorpayToolTestCase{ 219 | { 220 | Name: "successful payment capture", 221 | Request: map[string]interface{}{ 222 | "payment_id": "pay_G3P9vcIhRs3NV4", 223 | "amount": float64(1000), 224 | "currency": "INR", 225 | }, 226 | MockHttpClient: func() (*http.Client, *httptest.Server) { 227 | return mock.NewHTTPClient( 228 | mock.Endpoint{ 229 | Path: fmt.Sprintf(capturePaymentPathFmt, "pay_G3P9vcIhRs3NV4"), 230 | Method: "POST", 231 | Response: successfulCaptureResp, 232 | }, 233 | ) 234 | }, 235 | ExpectError: false, 236 | ExpectedResult: successfulCaptureResp, 237 | }, 238 | { 239 | Name: "payment already captured", 240 | Request: map[string]interface{}{ 241 | "payment_id": "pay_G3P9vcIhRs3NV4", 242 | "amount": float64(1000), 243 | "currency": "INR", 244 | }, 245 | MockHttpClient: func() (*http.Client, *httptest.Server) { 246 | return mock.NewHTTPClient( 247 | mock.Endpoint{ 248 | Path: fmt.Sprintf(capturePaymentPathFmt, "pay_G3P9vcIhRs3NV4"), 249 | Method: "POST", 250 | Response: alreadyCapturedResp, 251 | }, 252 | ) 253 | }, 254 | ExpectError: true, 255 | ExpectedErrMsg: "capturing payment failed: This payment has already been " + 256 | "captured", 257 | }, 258 | { 259 | Name: "missing payment_id parameter", 260 | Request: map[string]interface{}{ 261 | "amount": float64(1000), 262 | "currency": "INR", 263 | }, 264 | MockHttpClient: nil, // No HTTP client needed for validation error 265 | ExpectError: true, 266 | ExpectedErrMsg: "missing required parameter: payment_id", 267 | }, 268 | { 269 | Name: "missing amount parameter", 270 | Request: map[string]interface{}{ 271 | "payment_id": "pay_G3P9vcIhRs3NV4", 272 | "currency": "INR", 273 | }, 274 | MockHttpClient: nil, // No HTTP client needed for validation error 275 | ExpectError: true, 276 | ExpectedErrMsg: "missing required parameter: amount", 277 | }, 278 | { 279 | Name: "missing currency parameter", 280 | Request: map[string]interface{}{ 281 | "payment_id": "pay_G3P9vcIhRs3NV4", 282 | "amount": float64(1000), 283 | }, 284 | MockHttpClient: nil, // No HTTP client needed for validation error 285 | ExpectError: true, 286 | ExpectedErrMsg: "missing required parameter: currency", 287 | }, 288 | { 289 | Name: "multiple validation errors", 290 | Request: map[string]interface{}{ 291 | // All required parameters missing 292 | }, 293 | MockHttpClient: nil, // No HTTP client needed for validation error 294 | ExpectError: true, 295 | ExpectedErrMsg: "Validation errors:\n- " + 296 | "missing required parameter: payment_id\n- " + 297 | "missing required parameter: amount\n- " + 298 | "missing required parameter: currency", 299 | }, 300 | } 301 | 302 | for _, tc := range tests { 303 | t.Run(tc.Name, func(t *testing.T) { 304 | runToolTest(t, tc, CapturePayment, "Payment") 305 | }) 306 | } 307 | } 308 | 309 | func Test_UpdatePayment(t *testing.T) { 310 | updatePaymentPathFmt := fmt.Sprintf( 311 | "/%s%s/%%s", 312 | constants.VERSION_V1, 313 | constants.PAYMENT_URL, 314 | ) 315 | 316 | successfulUpdateResp := map[string]interface{}{ 317 | "id": "pay_KbCVlLqUbb3VhA", 318 | "entity": "payment", 319 | "amount": float64(400000), 320 | "currency": "INR", 321 | "status": "authorized", 322 | "order_id": nil, 323 | "invoice_id": nil, 324 | "international": false, 325 | "method": "emi", 326 | "amount_refunded": float64(0), 327 | "refund_status": nil, 328 | "captured": false, 329 | "description": "Test Transaction", 330 | "card_id": "card_KbCVlPnxWRlOpH", 331 | "bank": "HDFC", 332 | "wallet": nil, 333 | "vpa": nil, 334 | "email": "[email protected]", 335 | "contact": "+919000090000", 336 | "notes": map[string]interface{}{ 337 | "key1": "value1", 338 | "key2": "value2", 339 | }, 340 | "fee": nil, 341 | "tax": nil, 342 | "error_code": nil, 343 | "error_description": nil, 344 | "error_source": nil, 345 | "error_step": nil, 346 | "error_reason": nil, 347 | "acquirer_data": map[string]interface{}{ 348 | "auth_code": "205480", 349 | }, 350 | "emi_plan": map[string]interface{}{ 351 | "issuer": "HDFC", 352 | "type": "credit", 353 | "rate": float64(1500), 354 | "duration": float64(24), 355 | }, 356 | "created_at": float64(1667398779), 357 | } 358 | 359 | paymentNotFoundResp := map[string]interface{}{ 360 | "error": map[string]interface{}{ 361 | "code": "BAD_REQUEST_ERROR", 362 | "description": "The id provided does not exist", 363 | }, 364 | } 365 | 366 | tests := []RazorpayToolTestCase{ 367 | { 368 | Name: "successful payment notes update", 369 | Request: map[string]interface{}{ 370 | "payment_id": "pay_KbCVlLqUbb3VhA", 371 | "notes": map[string]interface{}{ 372 | "key1": "value1", 373 | "key2": "value2", 374 | }, 375 | }, 376 | MockHttpClient: func() (*http.Client, *httptest.Server) { 377 | return mock.NewHTTPClient( 378 | mock.Endpoint{ 379 | Path: fmt.Sprintf(updatePaymentPathFmt, "pay_KbCVlLqUbb3VhA"), 380 | Method: "PATCH", 381 | Response: successfulUpdateResp, 382 | }, 383 | ) 384 | }, 385 | ExpectError: false, 386 | ExpectedResult: successfulUpdateResp, 387 | }, 388 | { 389 | Name: "payment not found", 390 | Request: map[string]interface{}{ 391 | "payment_id": "pay_invalid", 392 | "notes": map[string]interface{}{ 393 | "key1": "value1", 394 | }, 395 | }, 396 | MockHttpClient: func() (*http.Client, *httptest.Server) { 397 | return mock.NewHTTPClient( 398 | mock.Endpoint{ 399 | Path: fmt.Sprintf(updatePaymentPathFmt, "pay_invalid"), 400 | Method: "PATCH", 401 | Response: paymentNotFoundResp, 402 | }, 403 | ) 404 | }, 405 | ExpectError: true, 406 | ExpectedErrMsg: "updating payment failed: The id provided does not exist", 407 | }, 408 | { 409 | Name: "missing payment_id parameter", 410 | Request: map[string]interface{}{ 411 | "notes": map[string]interface{}{ 412 | "key1": "value1", 413 | }, 414 | }, 415 | MockHttpClient: nil, // No HTTP client needed for validation error 416 | ExpectError: true, 417 | ExpectedErrMsg: "missing required parameter: payment_id", 418 | }, 419 | { 420 | Name: "missing notes parameter", 421 | Request: map[string]interface{}{ 422 | "payment_id": "pay_KbCVlLqUbb3VhA", 423 | }, 424 | MockHttpClient: nil, // No HTTP client needed for validation error 425 | ExpectError: true, 426 | ExpectedErrMsg: "missing required parameter: notes", 427 | }, 428 | { 429 | Name: "multiple validation errors", 430 | Request: map[string]interface{}{ 431 | // All required parameters missing 432 | }, 433 | MockHttpClient: nil, // No HTTP client needed for validation error 434 | ExpectError: true, 435 | ExpectedErrMsg: "Validation errors:\n- " + 436 | "missing required parameter: payment_id\n- " + 437 | "missing required parameter: notes", 438 | }, 439 | } 440 | 441 | for _, tc := range tests { 442 | t.Run(tc.Name, func(t *testing.T) { 443 | runToolTest(t, tc, UpdatePayment, "Payment") 444 | }) 445 | } 446 | } 447 | 448 | func Test_FetchAllPayments(t *testing.T) { 449 | fetchAllPaymentsPath := fmt.Sprintf( 450 | "/%s%s", 451 | constants.VERSION_V1, 452 | constants.PAYMENT_URL, 453 | ) 454 | 455 | // Sample response for successful fetch 456 | paymentsListResp := map[string]interface{}{ 457 | "entity": "collection", 458 | "count": float64(2), 459 | "items": []interface{}{ 460 | map[string]interface{}{ 461 | "id": "pay_KbCFyQ0t9Lmi1n", 462 | "entity": "payment", 463 | "amount": float64(1000), 464 | "currency": "INR", 465 | "status": "authorized", 466 | "order_id": nil, 467 | "invoice_id": nil, 468 | "international": false, 469 | "method": "netbanking", 470 | "amount_refunded": float64(0), 471 | "refund_status": nil, 472 | "captured": false, 473 | "description": "Test Transaction", 474 | "card_id": nil, 475 | "bank": "IBKL", 476 | "wallet": nil, 477 | "vpa": nil, 478 | "email": "[email protected]", 479 | "contact": "+919000090000", 480 | "notes": map[string]interface{}{ 481 | "address": "Razorpay Corporate Office", 482 | }, 483 | "fee": nil, 484 | "tax": nil, 485 | "error_code": nil, 486 | "error_description": nil, 487 | "error_source": nil, 488 | "error_step": nil, 489 | "error_reason": nil, 490 | "acquirer_data": map[string]interface{}{ 491 | "bank_transaction_id": "5733649", 492 | }, 493 | "created_at": float64(1667397881), 494 | }, 495 | map[string]interface{}{ 496 | "id": "pay_KbCEDHh1IrU4RJ", 497 | "entity": "payment", 498 | "amount": float64(1000), 499 | "currency": "INR", 500 | "status": "authorized", 501 | "order_id": nil, 502 | "invoice_id": nil, 503 | "international": false, 504 | "method": "upi", 505 | "amount_refunded": float64(0), 506 | "refund_status": nil, 507 | "captured": false, 508 | "description": "Test Transaction", 509 | "card_id": nil, 510 | "bank": nil, 511 | "wallet": nil, 512 | "vpa": "gaurav.kumar@okhdfcbank", 513 | "email": "[email protected]", 514 | "contact": "+919000090000", 515 | "notes": map[string]interface{}{ 516 | "address": "Razorpay Corporate Office", 517 | }, 518 | "fee": nil, 519 | "tax": nil, 520 | "error_code": nil, 521 | "error_description": nil, 522 | "error_source": nil, 523 | "error_step": nil, 524 | "error_reason": nil, 525 | "acquirer_data": map[string]interface{}{ 526 | "rrn": "230901495295", 527 | "upi_transaction_id": "6935B87A72C2A7BC83FA927AA264AD53", 528 | }, 529 | "created_at": float64(1667397781), 530 | }, 531 | }, 532 | } 533 | 534 | // Error response when parameters are invalid 535 | invalidParamsResp := map[string]interface{}{ 536 | "error": map[string]interface{}{ 537 | "code": "BAD_REQUEST_ERROR", 538 | "description": "from must be between 946684800 and 4765046400", 539 | }, 540 | } 541 | 542 | tests := []RazorpayToolTestCase{ 543 | { 544 | Name: "successful payments fetch with all parameters", 545 | Request: map[string]interface{}{ 546 | "from": float64(1593320020), 547 | "to": float64(1624856020), 548 | "count": float64(2), 549 | "skip": float64(1), 550 | }, 551 | MockHttpClient: func() (*http.Client, *httptest.Server) { 552 | return mock.NewHTTPClient( 553 | mock.Endpoint{ 554 | Path: fetchAllPaymentsPath, 555 | Method: "GET", 556 | Response: paymentsListResp, 557 | }, 558 | ) 559 | }, 560 | ExpectError: false, 561 | ExpectedResult: paymentsListResp, 562 | }, 563 | { 564 | Name: "payments fetch with invalid timestamp", 565 | Request: map[string]interface{}{ 566 | "from": float64(900000000), // Invalid timestamp (too early) 567 | "to": float64(1624856020), 568 | }, 569 | MockHttpClient: func() (*http.Client, *httptest.Server) { 570 | return mock.NewHTTPClient( 571 | mock.Endpoint{ 572 | Path: fetchAllPaymentsPath, 573 | Method: "GET", 574 | Response: invalidParamsResp, 575 | }, 576 | ) 577 | }, 578 | ExpectError: true, 579 | ExpectedErrMsg: "fetching payments failed: from must be between " + 580 | "946684800 and 4765046400", 581 | }, 582 | { 583 | Name: "multiple validation errors with wrong types", 584 | Request: map[string]interface{}{ 585 | "count": "not_a_number", 586 | "skip": "not_a_number", 587 | "from": "not_a_number", 588 | "to": "not_a_number", 589 | }, 590 | MockHttpClient: nil, // No HTTP client needed for validation error 591 | ExpectError: true, 592 | ExpectedErrMsg: "Validation errors:\n- " + 593 | "invalid parameter type: count\n- " + 594 | "invalid parameter type: skip\n- " + 595 | "invalid parameter type: from\n- " + 596 | "invalid parameter type: to", 597 | }, 598 | } 599 | 600 | for _, tc := range tests { 601 | t.Run(tc.Name, func(t *testing.T) { 602 | runToolTest(t, tc, FetchAllPayments, "Payments List") 603 | }) 604 | } 605 | } 606 | 607 | func Test_InitiatePayment(t *testing.T) { 608 | initiatePaymentPath := fmt.Sprintf( 609 | "/%s%s/create/json", 610 | constants.VERSION_V1, 611 | constants.PAYMENT_URL, 612 | ) 613 | 614 | createCustomerPath := fmt.Sprintf( 615 | "/%s%s", 616 | constants.VERSION_V1, 617 | constants.CUSTOMER_URL, 618 | ) 619 | 620 | customerResp := map[string]interface{}{ 621 | "id": "cust_1Aa00000000003", 622 | "entity": "customer", 623 | "name": "", 624 | "email": "", 625 | "contact": "9876543210", 626 | "gstin": nil, 627 | "notes": map[string]interface{}{}, 628 | "created_at": float64(1234567890), 629 | } 630 | 631 | successPaymentWithRedirectResp := map[string]interface{}{ 632 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 633 | "status": "created", 634 | "amount": float64(10000), 635 | "currency": "INR", 636 | "order_id": "order_129837127313912", 637 | "next": []interface{}{ 638 | map[string]interface{}{ 639 | "action": "redirect", 640 | "url": "https://api.razorpay.com/v1/payments/" + 641 | "pay_MT48CvBhIC98MQ/authenticate", 642 | }, 643 | }, 644 | } 645 | 646 | successPaymentWithoutNextResp := map[string]interface{}{ 647 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 648 | "status": "captured", 649 | "amount": float64(10000), 650 | "currency": "INR", 651 | "order_id": "order_129837127313912", 652 | } 653 | 654 | paymentErrorResp := map[string]interface{}{ 655 | "error": map[string]interface{}{ 656 | "code": "BAD_REQUEST_ERROR", 657 | "description": "Invalid token", 658 | }, 659 | } 660 | 661 | tests := []RazorpayToolTestCase{ 662 | { 663 | Name: "successful payment initiation without next actions", 664 | Request: map[string]interface{}{ 665 | "amount": 10000, 666 | "currency": "INR", 667 | "token": "token_MT48CvBhIC98MQ", 668 | "order_id": "order_129837127313912", 669 | "email": "[email protected]", 670 | "contact": "9876543210", 671 | }, 672 | MockHttpClient: func() (*http.Client, *httptest.Server) { 673 | return mock.NewHTTPClient( 674 | mock.Endpoint{ 675 | Path: createCustomerPath, 676 | Method: "POST", 677 | Response: customerResp, 678 | }, 679 | mock.Endpoint{ 680 | Path: initiatePaymentPath, 681 | Method: "POST", 682 | Response: successPaymentWithoutNextResp, 683 | }, 684 | ) 685 | }, 686 | ExpectError: false, 687 | ExpectedResult: map[string]interface{}{ 688 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 689 | "payment_details": successPaymentWithoutNextResp, 690 | "status": "payment_initiated", 691 | "message": "Payment initiated successfully using " + 692 | "S2S JSON v1 flow", 693 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 694 | "'submit_otp' to proceed to enter OTP if " + 695 | "OTP authentication is required.", 696 | "next_tool": "resend_otp", 697 | "next_tool_params": map[string]interface{}{ 698 | "payment_id": "pay_MT48CvBhIC98MQ", 699 | }, 700 | }, 701 | }, 702 | { 703 | Name: "successful payment initiation with redirect", 704 | Request: map[string]interface{}{ 705 | "amount": 10000, 706 | "token": "token_MT48CvBhIC98MQ", 707 | "order_id": "order_129837127313912", 708 | }, 709 | MockHttpClient: func() (*http.Client, *httptest.Server) { 710 | return mock.NewHTTPClient( 711 | mock.Endpoint{ 712 | Path: initiatePaymentPath, 713 | Method: "POST", 714 | Response: successPaymentWithRedirectResp, 715 | }, 716 | ) 717 | }, 718 | ExpectError: false, 719 | ExpectedResult: map[string]interface{}{ 720 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 721 | "payment_details": successPaymentWithRedirectResp, 722 | "status": "payment_initiated", 723 | "message": "Payment initiated. Redirect authentication is available. " + 724 | "Use the redirect URL provided in available_actions.", 725 | "available_actions": []interface{}{ 726 | map[string]interface{}{ 727 | "action": "redirect", 728 | "url": "https://api.razorpay.com/v1/payments/" + 729 | "pay_MT48CvBhIC98MQ/authenticate", 730 | }, 731 | }, 732 | }, 733 | }, 734 | { 735 | Name: "successful payment initiation with contact only", 736 | Request: map[string]interface{}{ 737 | "amount": 10000, 738 | "token": "token_MT48CvBhIC98MQ", 739 | "order_id": "order_129837127313912", 740 | "contact": "9876543210", 741 | }, 742 | MockHttpClient: func() (*http.Client, *httptest.Server) { 743 | return mock.NewHTTPClient( 744 | mock.Endpoint{ 745 | Path: createCustomerPath, 746 | Method: "POST", 747 | Response: customerResp, 748 | }, 749 | mock.Endpoint{ 750 | Path: initiatePaymentPath, 751 | Method: "POST", 752 | Response: successPaymentWithoutNextResp, 753 | }, 754 | ) 755 | }, 756 | ExpectError: false, 757 | ExpectedResult: map[string]interface{}{ 758 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 759 | "payment_details": successPaymentWithoutNextResp, 760 | "status": "payment_initiated", 761 | "message": "Payment initiated successfully using " + 762 | "S2S JSON v1 flow", 763 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 764 | "'submit_otp' to proceed to enter OTP if " + 765 | "OTP authentication is required.", 766 | "next_tool": "resend_otp", 767 | "next_tool_params": map[string]interface{}{ 768 | "payment_id": "pay_MT48CvBhIC98MQ", 769 | }, 770 | }, 771 | }, 772 | { 773 | Name: "payment initiation with API error", 774 | Request: map[string]interface{}{ 775 | "amount": 10000, 776 | "token": "token_invalid", 777 | "order_id": "order_129837127313912", 778 | }, 779 | MockHttpClient: func() (*http.Client, *httptest.Server) { 780 | return mock.NewHTTPClient( 781 | mock.Endpoint{ 782 | Path: initiatePaymentPath, 783 | Method: "POST", 784 | Response: paymentErrorResp, 785 | }, 786 | ) 787 | }, 788 | ExpectError: true, 789 | ExpectedErrMsg: "initiating payment failed:", 790 | }, 791 | { 792 | Name: "missing required amount parameter", 793 | Request: map[string]interface{}{ 794 | "token": "token_MT48CvBhIC98MQ", 795 | "order_id": "order_129837127313912", 796 | }, 797 | MockHttpClient: nil, 798 | ExpectError: true, 799 | ExpectedErrMsg: "missing required parameter: amount", 800 | }, 801 | { 802 | Name: "missing required order_id parameter", 803 | Request: map[string]interface{}{ 804 | "amount": 10000, 805 | "token": "token_MT48CvBhIC98MQ", 806 | }, 807 | MockHttpClient: nil, 808 | ExpectError: true, 809 | ExpectedErrMsg: "missing required parameter: order_id", 810 | }, 811 | { 812 | Name: "invalid amount parameter type", 813 | Request: map[string]interface{}{ 814 | "amount": "not_a_number", 815 | "token": "token_MT48CvBhIC98MQ", 816 | "order_id": "order_129837127313912", 817 | }, 818 | MockHttpClient: nil, 819 | ExpectError: true, 820 | ExpectedErrMsg: "invalid parameter type: amount", 821 | }, 822 | { 823 | Name: "multiple validation errors", 824 | Request: map[string]interface{}{ 825 | "amount": "not_a_number", 826 | "token": 123, 827 | "email": 456, 828 | }, 829 | MockHttpClient: nil, 830 | ExpectError: true, 831 | ExpectedErrMsg: "Validation errors:\n- invalid parameter type: amount\n- " + 832 | "invalid parameter type: token\n- " + 833 | "missing required parameter: order_id\n- " + 834 | "invalid parameter type: email", 835 | }, 836 | { 837 | Name: "successful UPI collect flow payment initiation", 838 | Request: map[string]interface{}{ 839 | "amount": 10000, 840 | "currency": "INR", 841 | "order_id": "order_129837127313912", 842 | "email": "[email protected]", 843 | "contact": "9876543210", 844 | "customer_id": "cust_RGCgP2osfPKFq2", 845 | "save": true, 846 | "vpa": "9876543210@ptsbi", 847 | }, 848 | MockHttpClient: func() (*http.Client, *httptest.Server) { 849 | successUpiCollectResp := map[string]interface{}{ 850 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 851 | "status": "created", 852 | "amount": float64(10000), 853 | "currency": "INR", 854 | "order_id": "order_129837127313912", 855 | "method": "upi", 856 | "next": []interface{}{ 857 | map[string]interface{}{ 858 | "action": "upi_collect", 859 | "url": "https://api.razorpay.com/v1/payments/" + 860 | "pay_MT48CvBhIC98MQ/authenticate", 861 | }, 862 | }, 863 | } 864 | return mock.NewHTTPClient( 865 | mock.Endpoint{ 866 | Path: createCustomerPath, 867 | Method: "POST", 868 | Response: customerResp, 869 | }, 870 | mock.Endpoint{ 871 | Path: initiatePaymentPath, 872 | Method: "POST", 873 | Response: successUpiCollectResp, 874 | }, 875 | ) 876 | }, 877 | ExpectError: false, 878 | ExpectedResult: map[string]interface{}{ 879 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 880 | "payment_details": map[string]interface{}{ 881 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 882 | "status": "created", 883 | "amount": float64(10000), 884 | "currency": "INR", 885 | "order_id": "order_129837127313912", 886 | "method": "upi", 887 | "next": []interface{}{ 888 | map[string]interface{}{ 889 | "action": "upi_collect", 890 | "url": "https://api.razorpay.com/v1/payments/" + 891 | "pay_MT48CvBhIC98MQ/authenticate", 892 | }, 893 | }, 894 | }, 895 | "status": "payment_initiated", 896 | "message": "Payment initiated. Available actions: [upi_collect]", 897 | "available_actions": []interface{}{ 898 | map[string]interface{}{ 899 | "action": "upi_collect", 900 | "url": "https://api.razorpay.com/v1/payments/" + 901 | "pay_MT48CvBhIC98MQ/authenticate", 902 | }, 903 | }, 904 | }, 905 | }, 906 | { 907 | Name: "successful UPI collect flow without token", 908 | Request: map[string]interface{}{ 909 | "amount": 10000, 910 | "order_id": "order_129837127313912", 911 | "contact": "9876543210", 912 | "vpa": "9876543210@ptsbi", 913 | }, 914 | MockHttpClient: func() (*http.Client, *httptest.Server) { 915 | successUpiCollectResp := map[string]interface{}{ 916 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 917 | "status": "created", 918 | "amount": float64(10000), 919 | "currency": "INR", 920 | "order_id": "order_129837127313912", 921 | "method": "upi", 922 | } 923 | return mock.NewHTTPClient( 924 | mock.Endpoint{ 925 | Path: createCustomerPath, 926 | Method: "POST", 927 | Response: customerResp, 928 | }, 929 | mock.Endpoint{ 930 | Path: initiatePaymentPath, 931 | Method: "POST", 932 | Response: successUpiCollectResp, 933 | }, 934 | ) 935 | }, 936 | ExpectError: false, 937 | ExpectedResult: map[string]interface{}{ 938 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 939 | "payment_details": map[string]interface{}{ 940 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 941 | "status": "created", 942 | "amount": float64(10000), 943 | "currency": "INR", 944 | "order_id": "order_129837127313912", 945 | "method": "upi", 946 | }, 947 | "status": "payment_initiated", 948 | "message": "Payment initiated successfully using " + 949 | "S2S JSON v1 flow", 950 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 951 | "'submit_otp' to proceed to enter OTP if " + 952 | "OTP authentication is required.", 953 | "next_tool": "resend_otp", 954 | "next_tool_params": map[string]interface{}{ 955 | "payment_id": "pay_MT48CvBhIC98MQ", 956 | }, 957 | }, 958 | }, 959 | { 960 | Name: "UPI collect flow with all optional parameters", 961 | Request: map[string]interface{}{ 962 | "amount": 10000, 963 | "currency": "INR", 964 | "order_id": "order_129837127313912", 965 | "email": "[email protected]", 966 | "contact": "9876543210", 967 | "customer_id": "cust_RGCgP2osfPKFq2", 968 | "save": false, 969 | "vpa": "test@paytm", 970 | }, 971 | MockHttpClient: func() (*http.Client, *httptest.Server) { 972 | successUpiCollectResp := map[string]interface{}{ 973 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 974 | "status": "created", 975 | "amount": float64(10000), 976 | "currency": "INR", 977 | "order_id": "order_129837127313912", 978 | "method": "upi", 979 | } 980 | return mock.NewHTTPClient( 981 | mock.Endpoint{ 982 | Path: createCustomerPath, 983 | Method: "POST", 984 | Response: customerResp, 985 | }, 986 | mock.Endpoint{ 987 | Path: initiatePaymentPath, 988 | Method: "POST", 989 | Response: successUpiCollectResp, 990 | }, 991 | ) 992 | }, 993 | ExpectError: false, 994 | ExpectedResult: map[string]interface{}{ 995 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 996 | "payment_details": map[string]interface{}{ 997 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 998 | "status": "created", 999 | "amount": float64(10000), 1000 | "currency": "INR", 1001 | "order_id": "order_129837127313912", 1002 | "method": "upi", 1003 | }, 1004 | "status": "payment_initiated", 1005 | "message": "Payment initiated successfully using " + 1006 | "S2S JSON v1 flow", 1007 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 1008 | "'submit_otp' to proceed to enter OTP if " + 1009 | "OTP authentication is required.", 1010 | "next_tool": "resend_otp", 1011 | "next_tool_params": map[string]interface{}{ 1012 | "payment_id": "pay_MT48CvBhIC98MQ", 1013 | }, 1014 | }, 1015 | }, 1016 | { 1017 | Name: "invalid save parameter type", 1018 | Request: map[string]interface{}{ 1019 | "amount": 10000, 1020 | "order_id": "order_129837127313912", 1021 | "save": "invalid_string_instead_of_bool", 1022 | }, 1023 | MockHttpClient: nil, 1024 | ExpectError: true, 1025 | ExpectedErrMsg: "invalid parameter type: save", 1026 | }, 1027 | { 1028 | Name: "invalid customer_id parameter type", 1029 | Request: map[string]interface{}{ 1030 | "amount": 10000, 1031 | "order_id": "order_129837127313912", 1032 | "customer_id": 123, 1033 | }, 1034 | MockHttpClient: nil, 1035 | ExpectError: true, 1036 | ExpectedErrMsg: "invalid parameter type: customer_id", 1037 | }, 1038 | { 1039 | Name: "successful UPI intent flow payment initiation", 1040 | Request: map[string]interface{}{ 1041 | "amount": 12000, 1042 | "currency": "INR", 1043 | "order_id": "order_INTENT123", 1044 | "email": "[email protected]", 1045 | "contact": "9876543210", 1046 | "upi_intent": true, 1047 | }, 1048 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1049 | successUpiIntentResp := map[string]interface{}{ 1050 | "razorpay_payment_id": "pay_INTENT123", 1051 | "status": "created", 1052 | "amount": float64(12000), 1053 | "currency": "INR", 1054 | "order_id": "order_INTENT123", 1055 | "method": "upi", 1056 | "upi": map[string]interface{}{ 1057 | "flow": "intent", 1058 | }, 1059 | "next": []interface{}{ 1060 | map[string]interface{}{ 1061 | "action": "upi_intent", 1062 | "url": "https://api.razorpay.com/v1/payments/" + 1063 | "pay_INTENT123/upi_intent", 1064 | }, 1065 | }, 1066 | } 1067 | return mock.NewHTTPClient( 1068 | mock.Endpoint{ 1069 | Path: createCustomerPath, 1070 | Method: "POST", 1071 | Response: customerResp, 1072 | }, 1073 | mock.Endpoint{ 1074 | Path: initiatePaymentPath, 1075 | Method: "POST", 1076 | Response: successUpiIntentResp, 1077 | }, 1078 | ) 1079 | }, 1080 | ExpectError: false, 1081 | ExpectedResult: map[string]interface{}{ 1082 | "razorpay_payment_id": "pay_INTENT123", 1083 | "payment_details": map[string]interface{}{ 1084 | "razorpay_payment_id": "pay_INTENT123", 1085 | "status": "created", 1086 | "amount": float64(12000), 1087 | "currency": "INR", 1088 | "order_id": "order_INTENT123", 1089 | "method": "upi", 1090 | "upi": map[string]interface{}{ 1091 | "flow": "intent", 1092 | }, 1093 | "next": []interface{}{ 1094 | map[string]interface{}{ 1095 | "action": "upi_intent", 1096 | "url": "https://api.razorpay.com/v1/payments/" + 1097 | "pay_INTENT123/upi_intent", 1098 | }, 1099 | }, 1100 | }, 1101 | "status": "payment_initiated", 1102 | "message": "Payment initiated. Available actions: [upi_intent]", 1103 | "available_actions": []interface{}{ 1104 | map[string]interface{}{ 1105 | "action": "upi_intent", 1106 | "url": "https://api.razorpay.com/v1/payments/" + 1107 | "pay_INTENT123/upi_intent", 1108 | }, 1109 | }, 1110 | }, 1111 | }, 1112 | { 1113 | Name: "invalid upi_intent parameter type", 1114 | Request: map[string]interface{}{ 1115 | "amount": 10000, 1116 | "order_id": "order_129837127313912", 1117 | "upi_intent": "invalid_string", 1118 | }, 1119 | MockHttpClient: nil, 1120 | ExpectError: true, 1121 | ExpectedErrMsg: "invalid parameter type: upi_intent", 1122 | }, 1123 | { 1124 | Name: "successful payment initiation with force_terminal_id " + 1125 | "for single block multiple debit", 1126 | Request: map[string]interface{}{ 1127 | "amount": 10000, 1128 | "currency": "INR", 1129 | "order_id": "order_129837127313912", 1130 | "email": "[email protected]", 1131 | "contact": "9876543210", 1132 | "customer_id": "cust_RGCgP2osfPKFq2", 1133 | "recurring": true, 1134 | "force_terminal_id": "term_ABCD1234256732", 1135 | }, 1136 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1137 | successPaymentWithTerminalResp := map[string]interface{}{ 1138 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1139 | "status": "created", 1140 | "amount": float64(10000), 1141 | "currency": "INR", 1142 | "order_id": "order_129837127313912", 1143 | "method": "upi", 1144 | "force_terminal_id": "term_ABCD1234256732", 1145 | } 1146 | return mock.NewHTTPClient( 1147 | mock.Endpoint{ 1148 | Path: initiatePaymentPath, 1149 | Method: "POST", 1150 | Response: successPaymentWithTerminalResp, 1151 | }, 1152 | ) 1153 | }, 1154 | ExpectError: false, 1155 | ExpectedResult: map[string]interface{}{ 1156 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1157 | "payment_details": map[string]interface{}{ 1158 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1159 | "status": "created", 1160 | "amount": float64(10000), 1161 | "currency": "INR", 1162 | "order_id": "order_129837127313912", 1163 | "method": "upi", 1164 | "force_terminal_id": "term_ABCD1234256732", 1165 | }, 1166 | "status": "payment_initiated", 1167 | "message": "Payment initiated successfully using S2S JSON v1 flow", 1168 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 1169 | "'submit_otp' to proceed to enter OTP if " + 1170 | "OTP authentication is required.", 1171 | "next_tool": "resend_otp", 1172 | "next_tool_params": map[string]interface{}{ 1173 | "payment_id": "pay_MT48CvBhIC98MQ", 1174 | }, 1175 | }, 1176 | }, 1177 | { 1178 | Name: "invalid force_terminal_id parameter type", 1179 | Request: map[string]interface{}{ 1180 | "amount": 10000, 1181 | "order_id": "order_129837127313912", 1182 | "force_terminal_id": 123, 1183 | }, 1184 | MockHttpClient: nil, 1185 | ExpectError: true, 1186 | ExpectedErrMsg: "invalid parameter type: force_terminal_id", 1187 | }, 1188 | } 1189 | 1190 | for _, tc := range tests { 1191 | t.Run(tc.Name, func(t *testing.T) { 1192 | runToolTest(t, tc, InitiatePayment, "Payment Initiation") 1193 | }) 1194 | } 1195 | } 1196 | 1197 | func Test_SubmitOtp(t *testing.T) { 1198 | submitOtpPathFmt := fmt.Sprintf( 1199 | "/%s%s/%%s/otp/submit", 1200 | constants.VERSION_V1, 1201 | constants.PAYMENT_URL, 1202 | ) 1203 | 1204 | successOtpSubmitResp := map[string]interface{}{ 1205 | "id": "pay_MT48CvBhIC98MQ", 1206 | "entity": "payment", 1207 | "amount": float64(10000), 1208 | "currency": "INR", 1209 | "status": "authorized", 1210 | "order_id": "order_129837127313912", 1211 | "description": "Test payment", 1212 | "method": "card", 1213 | "amount_refunded": float64(0), 1214 | "refund_status": nil, 1215 | "captured": false, 1216 | "email": "[email protected]", 1217 | "contact": "9876543210", 1218 | "fee": float64(236), 1219 | "tax": float64(36), 1220 | "error_code": nil, 1221 | "error_description": nil, 1222 | "created_at": float64(1234567890), 1223 | } 1224 | 1225 | otpVerificationFailedResp := map[string]interface{}{ 1226 | "error": map[string]interface{}{ 1227 | "code": "BAD_REQUEST_ERROR", 1228 | "description": "Invalid OTP provided", 1229 | "field": "otp", 1230 | }, 1231 | } 1232 | 1233 | paymentNotFoundResp := map[string]interface{}{ 1234 | "error": map[string]interface{}{ 1235 | "code": "BAD_REQUEST_ERROR", 1236 | "description": "Payment not found", 1237 | }, 1238 | } 1239 | 1240 | tests := []RazorpayToolTestCase{ 1241 | { 1242 | Name: "successful OTP submission", 1243 | Request: map[string]interface{}{ 1244 | "payment_id": "pay_MT48CvBhIC98MQ", 1245 | "otp_string": "123456", 1246 | }, 1247 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1248 | return mock.NewHTTPClient( 1249 | mock.Endpoint{ 1250 | Path: fmt.Sprintf(submitOtpPathFmt, "pay_MT48CvBhIC98MQ"), 1251 | Method: "POST", 1252 | Response: successOtpSubmitResp, 1253 | }, 1254 | ) 1255 | }, 1256 | ExpectError: false, 1257 | ExpectedResult: map[string]interface{}{ 1258 | "payment_id": "pay_MT48CvBhIC98MQ", 1259 | "status": "success", 1260 | "message": "OTP verified successfully.", 1261 | "response_data": successOtpSubmitResp, 1262 | }, 1263 | }, 1264 | { 1265 | Name: "OTP verification failed - invalid OTP", 1266 | Request: map[string]interface{}{ 1267 | "payment_id": "pay_MT48CvBhIC98MQ", 1268 | "otp_string": "000000", 1269 | }, 1270 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1271 | return mock.NewHTTPClient( 1272 | mock.Endpoint{ 1273 | Path: fmt.Sprintf(submitOtpPathFmt, "pay_MT48CvBhIC98MQ"), 1274 | Method: "POST", 1275 | Response: otpVerificationFailedResp, 1276 | }, 1277 | ) 1278 | }, 1279 | ExpectError: true, 1280 | ExpectedErrMsg: "OTP verification failed: Invalid OTP provided", 1281 | }, 1282 | { 1283 | Name: "payment not found", 1284 | Request: map[string]interface{}{ 1285 | "payment_id": "pay_invalid", 1286 | "otp_string": "123456", 1287 | }, 1288 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1289 | return mock.NewHTTPClient( 1290 | mock.Endpoint{ 1291 | Path: fmt.Sprintf(submitOtpPathFmt, "pay_invalid"), 1292 | Method: "POST", 1293 | Response: paymentNotFoundResp, 1294 | }, 1295 | ) 1296 | }, 1297 | ExpectError: true, 1298 | ExpectedErrMsg: "OTP verification failed: Payment not found", 1299 | }, 1300 | { 1301 | Name: "missing payment_id parameter", 1302 | Request: map[string]interface{}{ 1303 | "otp_string": "123456", 1304 | }, 1305 | MockHttpClient: nil, // No HTTP client needed for validation error 1306 | ExpectError: true, 1307 | ExpectedErrMsg: "missing required parameter: payment_id", 1308 | }, 1309 | { 1310 | Name: "missing otp_string parameter", 1311 | Request: map[string]interface{}{ 1312 | "payment_id": "pay_MT48CvBhIC98MQ", 1313 | }, 1314 | MockHttpClient: nil, // No HTTP client needed for validation error 1315 | ExpectError: true, 1316 | ExpectedErrMsg: "missing required parameter: otp_string", 1317 | }, 1318 | { 1319 | Name: "missing both required parameters", 1320 | Request: map[string]interface{}{}, 1321 | MockHttpClient: nil, // No HTTP client needed for validation error 1322 | ExpectError: true, 1323 | ExpectedErrMsg: "missing required parameter: otp_string", 1324 | }, 1325 | { 1326 | Name: "empty otp_string", 1327 | Request: map[string]interface{}{ 1328 | "payment_id": "pay_MT48CvBhIC98MQ", 1329 | "otp_string": "", 1330 | }, 1331 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1332 | return mock.NewHTTPClient( 1333 | mock.Endpoint{ 1334 | Path: fmt.Sprintf(submitOtpPathFmt, "pay_MT48CvBhIC98MQ"), 1335 | Method: "POST", 1336 | Response: map[string]interface{}{ 1337 | "error": map[string]interface{}{ 1338 | "code": "BAD_REQUEST_ERROR", 1339 | "description": "Authentication failed", 1340 | }, 1341 | }, 1342 | }, 1343 | ) 1344 | }, 1345 | ExpectError: true, 1346 | ExpectedErrMsg: "OTP verification failed: Authentication failed", 1347 | }, 1348 | { 1349 | Name: "empty payment_id", 1350 | Request: map[string]interface{}{ 1351 | "payment_id": "", 1352 | "otp_string": "123456", 1353 | }, 1354 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1355 | return mock.NewHTTPClient( 1356 | mock.Endpoint{ 1357 | Path: fmt.Sprintf(submitOtpPathFmt, ""), 1358 | Method: "POST", 1359 | Response: map[string]interface{}{ 1360 | "error": map[string]interface{}{ 1361 | "code": "BAD_REQUEST_ERROR", 1362 | "description": "", 1363 | }, 1364 | }, 1365 | }, 1366 | ) 1367 | }, 1368 | ExpectError: true, 1369 | ExpectedErrMsg: "OTP verification failed:", 1370 | }, 1371 | } 1372 | 1373 | for _, tc := range tests { 1374 | t.Run(tc.Name, func(t *testing.T) { 1375 | runToolTest(t, tc, SubmitOtp, "OTP Submission") 1376 | }) 1377 | } 1378 | } 1379 | 1380 | func Test_InitiatePaymentWithVPA(t *testing.T) { 1381 | initiatePaymentPath := fmt.Sprintf( 1382 | "/%s%s/create/json", 1383 | constants.VERSION_V1, 1384 | constants.PAYMENT_URL, 1385 | ) 1386 | 1387 | createCustomerPath := fmt.Sprintf( 1388 | "/%s%s", 1389 | constants.VERSION_V1, 1390 | constants.CUSTOMER_URL, 1391 | ) 1392 | 1393 | customerResp := map[string]interface{}{ 1394 | "id": "cust_1Aa00000000003", 1395 | "entity": "customer", 1396 | "name": "", 1397 | "email": "", 1398 | "contact": "9876543210", 1399 | "gstin": nil, 1400 | "notes": map[string]interface{}{}, 1401 | "created_at": float64(1234567890), 1402 | } 1403 | 1404 | testCases := []RazorpayToolTestCase{ 1405 | { 1406 | Name: "successful UPI payment with VPA parameter", 1407 | Request: map[string]interface{}{ 1408 | "amount": 10000, 1409 | "order_id": "order_129837127313912", 1410 | "vpa": "9876543210@ptsbi", 1411 | "email": "[email protected]", 1412 | "contact": "9876543210", 1413 | }, 1414 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1415 | successUpiVpaResp := map[string]interface{}{ 1416 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1417 | "status": "created", 1418 | "amount": float64(10000), 1419 | "currency": "INR", 1420 | "order_id": "order_129837127313912", 1421 | "method": "upi", 1422 | "email": "[email protected]", 1423 | "contact": "9876543210", 1424 | "upi_transaction_id": nil, 1425 | "upi": map[string]interface{}{ 1426 | "flow": "collect", 1427 | "expiry_time": "6", 1428 | "vpa": "9876543210@ptsbi", 1429 | }, 1430 | "next": []interface{}{ 1431 | map[string]interface{}{ 1432 | "action": "upi_collect", 1433 | "url": "https://api.razorpay.com/v1/payments/" + 1434 | "pay_MT48CvBhIC98MQ/otp_generate", 1435 | }, 1436 | }, 1437 | } 1438 | return mock.NewHTTPClient( 1439 | mock.Endpoint{ 1440 | Path: createCustomerPath, 1441 | Method: "POST", 1442 | Response: customerResp, 1443 | }, 1444 | mock.Endpoint{ 1445 | Path: initiatePaymentPath, 1446 | Method: "POST", 1447 | Response: successUpiVpaResp, 1448 | }, 1449 | ) 1450 | }, 1451 | ExpectError: false, 1452 | ExpectedResult: map[string]interface{}{ 1453 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1454 | "payment_details": map[string]interface{}{ 1455 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1456 | "status": "created", 1457 | "amount": float64(10000), 1458 | "currency": "INR", 1459 | "order_id": "order_129837127313912", 1460 | "method": "upi", 1461 | "email": "[email protected]", 1462 | "contact": "9876543210", 1463 | "upi_transaction_id": nil, 1464 | "upi": map[string]interface{}{ 1465 | "flow": "collect", 1466 | "expiry_time": "6", 1467 | "vpa": "9876543210@ptsbi", 1468 | }, 1469 | "next": []interface{}{ 1470 | map[string]interface{}{ 1471 | "action": "upi_collect", 1472 | "url": "https://api.razorpay.com/v1/payments/" + 1473 | "pay_MT48CvBhIC98MQ/otp_generate", 1474 | }, 1475 | }, 1476 | }, 1477 | "status": "payment_initiated", 1478 | "message": "Payment initiated. Available actions: [upi_collect]", 1479 | "available_actions": []interface{}{ 1480 | map[string]interface{}{ 1481 | "action": "upi_collect", 1482 | "url": "https://api.razorpay.com/v1/payments/" + 1483 | "pay_MT48CvBhIC98MQ/otp_generate", 1484 | }, 1485 | }, 1486 | }, 1487 | }, 1488 | { 1489 | Name: "UPI payment with VPA and custom currency", 1490 | Request: map[string]interface{}{ 1491 | "amount": 20000, 1492 | "currency": "INR", 1493 | "order_id": "order_ABC123XYZ456", 1494 | "vpa": "test@upi", 1495 | }, 1496 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1497 | successUpiVpaResp := map[string]interface{}{ 1498 | "razorpay_payment_id": "pay_ABC123XYZ456", 1499 | "status": "created", 1500 | "amount": float64(20000), 1501 | "currency": "INR", 1502 | "order_id": "order_ABC123XYZ456", 1503 | "method": "upi", 1504 | "upi": map[string]interface{}{ 1505 | "flow": "collect", 1506 | "expiry_time": "6", 1507 | "vpa": "test@upi", 1508 | }, 1509 | } 1510 | return mock.NewHTTPClient( 1511 | mock.Endpoint{ 1512 | Path: initiatePaymentPath, 1513 | Method: "POST", 1514 | Response: successUpiVpaResp, 1515 | }, 1516 | ) 1517 | }, 1518 | ExpectError: false, 1519 | ExpectedResult: map[string]interface{}{ 1520 | "razorpay_payment_id": "pay_ABC123XYZ456", 1521 | "payment_details": map[string]interface{}{ 1522 | "razorpay_payment_id": "pay_ABC123XYZ456", 1523 | "status": "created", 1524 | "amount": float64(20000), 1525 | "currency": "INR", 1526 | "order_id": "order_ABC123XYZ456", 1527 | "method": "upi", 1528 | "upi": map[string]interface{}{ 1529 | "flow": "collect", 1530 | "expiry_time": "6", 1531 | "vpa": "test@upi", 1532 | }, 1533 | }, 1534 | "status": "payment_initiated", 1535 | "message": "Payment initiated successfully using " + 1536 | "S2S JSON v1 flow", 1537 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 1538 | "'submit_otp' to proceed to enter OTP if " + 1539 | "OTP authentication is required.", 1540 | "next_tool": "resend_otp", 1541 | "next_tool_params": map[string]interface{}{ 1542 | "payment_id": "pay_ABC123XYZ456", 1543 | }, 1544 | }, 1545 | }, 1546 | { 1547 | Name: "missing VPA parameter value", 1548 | Request: map[string]interface{}{ 1549 | "amount": 10000, 1550 | "order_id": "order_129837127313912", 1551 | "vpa": "", 1552 | }, 1553 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1554 | successRegularResp := map[string]interface{}{ 1555 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1556 | "status": "created", 1557 | "amount": float64(10000), 1558 | "currency": "INR", 1559 | "order_id": "order_129837127313912", 1560 | } 1561 | return mock.NewHTTPClient( 1562 | mock.Endpoint{ 1563 | Path: initiatePaymentPath, 1564 | Method: "POST", 1565 | Response: successRegularResp, 1566 | }, 1567 | ) 1568 | }, 1569 | ExpectError: false, // Empty VPA should not trigger UPI logic 1570 | ExpectedResult: map[string]interface{}{ 1571 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1572 | "payment_details": map[string]interface{}{ 1573 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1574 | "status": "created", 1575 | "amount": float64(10000), 1576 | "currency": "INR", 1577 | "order_id": "order_129837127313912", 1578 | }, 1579 | "status": "payment_initiated", 1580 | "message": "Payment initiated successfully using " + 1581 | "S2S JSON v1 flow", 1582 | "next_step": "Use 'resend_otp' to regenerate OTP or " + 1583 | "'submit_otp' to proceed to enter OTP if " + 1584 | "OTP authentication is required.", 1585 | "next_tool": "resend_otp", 1586 | "next_tool_params": map[string]interface{}{ 1587 | "payment_id": "pay_MT48CvBhIC98MQ", 1588 | }, 1589 | }, 1590 | }, 1591 | { 1592 | Name: "VPA parameter automatically sets UPI method", 1593 | Request: map[string]interface{}{ 1594 | "amount": 15000, 1595 | "order_id": "order_OVERRIDE123", 1596 | "vpa": "new@upi", 1597 | }, 1598 | MockHttpClient: func() (*http.Client, *httptest.Server) { 1599 | successUpiOverrideResp := map[string]interface{}{ 1600 | "razorpay_payment_id": "pay_OVERRIDE123", 1601 | "status": "created", 1602 | "amount": float64(15000), 1603 | "currency": "INR", 1604 | "order_id": "order_OVERRIDE123", 1605 | "method": "upi", // Should be set to UPI by VPA 1606 | "upi": map[string]interface{}{ 1607 | "flow": "collect", // Default flow 1608 | "expiry_time": "6", // Default expiry 1609 | "vpa": "new@upi", // VPA from parameter 1610 | }, 1611 | "next": []interface{}{ 1612 | map[string]interface{}{ 1613 | "action": "upi_collect", 1614 | "url": "https://api.razorpay.com/v1/payments/" + 1615 | "pay_OVERRIDE123/otp_generate", 1616 | }, 1617 | }, 1618 | } 1619 | return mock.NewHTTPClient( 1620 | mock.Endpoint{ 1621 | Path: initiatePaymentPath, 1622 | Method: "POST", 1623 | Response: successUpiOverrideResp, 1624 | }, 1625 | ) 1626 | }, 1627 | ExpectError: false, 1628 | ExpectedResult: map[string]interface{}{ 1629 | "razorpay_payment_id": "pay_OVERRIDE123", 1630 | "payment_details": map[string]interface{}{ 1631 | "razorpay_payment_id": "pay_OVERRIDE123", 1632 | "status": "created", 1633 | "amount": float64(15000), 1634 | "currency": "INR", 1635 | "order_id": "order_OVERRIDE123", 1636 | "method": "upi", 1637 | "upi": map[string]interface{}{ 1638 | "flow": "collect", 1639 | "expiry_time": "6", 1640 | "vpa": "new@upi", 1641 | }, 1642 | "next": []interface{}{ 1643 | map[string]interface{}{ 1644 | "action": "upi_collect", 1645 | "url": "https://api.razorpay.com/v1/payments/" + 1646 | "pay_OVERRIDE123/otp_generate", 1647 | }, 1648 | }, 1649 | }, 1650 | "status": "payment_initiated", 1651 | "message": "Payment initiated. Available actions: [upi_collect]", 1652 | "available_actions": []interface{}{ 1653 | map[string]interface{}{ 1654 | "action": "upi_collect", 1655 | "url": "https://api.razorpay.com/v1/payments/" + 1656 | "pay_OVERRIDE123/otp_generate", 1657 | }, 1658 | }, 1659 | }, 1660 | }, 1661 | } 1662 | 1663 | for _, tc := range testCases { 1664 | t.Run(tc.Name, func(t *testing.T) { 1665 | runToolTest(t, tc, InitiatePayment, "Payment") 1666 | }) 1667 | } 1668 | } 1669 | 1670 | // Test helper functions for better coverage 1671 | func Test_extractPaymentID(t *testing.T) { 1672 | tests := []struct { 1673 | name string 1674 | payment map[string]interface{} 1675 | expected string 1676 | }{ 1677 | { 1678 | name: "valid payment ID", 1679 | payment: map[string]interface{}{ 1680 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 1681 | "status": "created", 1682 | }, 1683 | expected: "pay_MT48CvBhIC98MQ", 1684 | }, 1685 | { 1686 | name: "missing payment ID", 1687 | payment: map[string]interface{}{ 1688 | "status": "created", 1689 | }, 1690 | expected: "", 1691 | }, 1692 | { 1693 | name: "nil payment ID", 1694 | payment: map[string]interface{}{ 1695 | "razorpay_payment_id": nil, 1696 | "status": "created", 1697 | }, 1698 | expected: "", 1699 | }, 1700 | { 1701 | name: "empty payment map", 1702 | payment: map[string]interface{}{}, 1703 | expected: "", 1704 | }, 1705 | } 1706 | 1707 | for _, tt := range tests { 1708 | t.Run(tt.name, func(t *testing.T) { 1709 | result := extractPaymentID(tt.payment) 1710 | if result != tt.expected { 1711 | t.Errorf("extractPaymentID() = %v, want %v", result, tt.expected) 1712 | } 1713 | }) 1714 | } 1715 | } 1716 | 1717 | func Test_buildInitiatePaymentResponse(t *testing.T) { 1718 | tests := []struct { 1719 | name string 1720 | payment map[string]interface{} 1721 | paymentID string 1722 | actions []map[string]interface{} 1723 | expectedMsg string 1724 | expectedOtpURL string 1725 | }{ 1726 | { 1727 | name: "payment with OTP action", 1728 | payment: map[string]interface{}{ 1729 | "id": "pay_MT48CvBhIC98MQ", 1730 | "status": "created", 1731 | }, 1732 | paymentID: "pay_MT48CvBhIC98MQ", 1733 | actions: []map[string]interface{}{ 1734 | { 1735 | "action": "otp_generate", 1736 | "url": "https://api.razorpay.com/v1/payments/" + 1737 | "pay_MT48CvBhIC98MQ/otp_generate", 1738 | }, 1739 | }, 1740 | expectedMsg: "Payment initiated. OTP authentication is available. " + 1741 | "Use the 'submit_otp' tool to submit OTP received by the customer " + 1742 | "for authentication.", 1743 | expectedOtpURL: "https://api.razorpay.com/v1/payments/" + 1744 | "pay_MT48CvBhIC98MQ/otp_generate", 1745 | }, 1746 | { 1747 | name: "payment with redirect action", 1748 | payment: map[string]interface{}{ 1749 | "id": "pay_MT48CvBhIC98MQ", 1750 | "status": "created", 1751 | }, 1752 | paymentID: "pay_MT48CvBhIC98MQ", 1753 | actions: []map[string]interface{}{ 1754 | { 1755 | "action": "redirect", 1756 | "url": "https://api.razorpay.com/v1/payments/" + 1757 | "pay_MT48CvBhIC98MQ/authenticate", 1758 | }, 1759 | }, 1760 | expectedMsg: "Payment initiated. Redirect authentication is available. " + 1761 | "Use the redirect URL provided in available_actions.", 1762 | expectedOtpURL: "", 1763 | }, 1764 | { 1765 | name: "payment with UPI collect action", 1766 | payment: map[string]interface{}{ 1767 | "id": "pay_MT48CvBhIC98MQ", 1768 | "status": "created", 1769 | }, 1770 | paymentID: "pay_MT48CvBhIC98MQ", 1771 | actions: []map[string]interface{}{ 1772 | { 1773 | "action": "upi_collect", 1774 | "url": "https://api.razorpay.com/v1/payments/" + 1775 | "pay_MT48CvBhIC98MQ/authenticate", 1776 | }, 1777 | }, 1778 | expectedMsg: "Payment initiated. Available actions: [upi_collect]", 1779 | expectedOtpURL: "", 1780 | }, 1781 | { 1782 | name: "payment with multiple actions including OTP", 1783 | payment: map[string]interface{}{ 1784 | "id": "pay_MT48CvBhIC98MQ", 1785 | "status": "created", 1786 | }, 1787 | paymentID: "pay_MT48CvBhIC98MQ", 1788 | actions: []map[string]interface{}{ 1789 | { 1790 | "action": "otp_generate", 1791 | "url": "https://api.razorpay.com/v1/payments/" + 1792 | "pay_MT48CvBhIC98MQ/otp_generate", 1793 | }, 1794 | { 1795 | "action": "redirect", 1796 | "url": "https://api.razorpay.com/v1/payments/" + 1797 | "pay_MT48CvBhIC98MQ/authenticate", 1798 | }, 1799 | }, 1800 | expectedMsg: "Payment initiated. OTP authentication is available. " + 1801 | "Use the 'submit_otp' tool to submit OTP received by the customer " + 1802 | "for authentication.", 1803 | expectedOtpURL: "https://api.razorpay.com/v1/payments/" + 1804 | "pay_MT48CvBhIC98MQ/otp_generate", 1805 | }, 1806 | { 1807 | name: "payment with no actions", 1808 | payment: map[string]interface{}{ 1809 | "id": "pay_MT48CvBhIC98MQ", 1810 | "status": "captured", 1811 | }, 1812 | paymentID: "pay_MT48CvBhIC98MQ", 1813 | actions: []map[string]interface{}{}, 1814 | expectedMsg: "Payment initiated successfully using S2S JSON v1 flow", 1815 | expectedOtpURL: "", 1816 | }, 1817 | { 1818 | name: "payment with unknown action", 1819 | payment: map[string]interface{}{ 1820 | "id": "pay_MT48CvBhIC98MQ", 1821 | "status": "created", 1822 | }, 1823 | paymentID: "pay_MT48CvBhIC98MQ", 1824 | actions: []map[string]interface{}{ 1825 | { 1826 | "action": "unknown_action", 1827 | "url": "https://api.razorpay.com/v1/payments/" + 1828 | "pay_MT48CvBhIC98MQ/unknown", 1829 | }, 1830 | }, 1831 | expectedMsg: "Payment initiated. Available actions: [unknown_action]", 1832 | expectedOtpURL: "", 1833 | }, 1834 | } 1835 | 1836 | for _, tt := range tests { 1837 | t.Run(tt.name, func(t *testing.T) { 1838 | response, otpURL := buildInitiatePaymentResponse( 1839 | tt.payment, tt.paymentID, tt.actions) 1840 | 1841 | // Check basic response structure 1842 | if response["razorpay_payment_id"] != tt.paymentID { 1843 | t.Errorf("Expected payment ID %s, got %v", tt.paymentID, 1844 | response["razorpay_payment_id"]) 1845 | } 1846 | 1847 | if response["status"] != "payment_initiated" { 1848 | t.Errorf("Expected status 'payment_initiated', got %v", response["status"]) 1849 | } 1850 | 1851 | // Check message 1852 | if response["message"] != tt.expectedMsg { 1853 | t.Errorf("Expected message %s, got %v", tt.expectedMsg, response["message"]) 1854 | } 1855 | 1856 | // Check OTP URL 1857 | if otpURL != tt.expectedOtpURL { 1858 | t.Errorf("Expected OTP URL %s, got %s", tt.expectedOtpURL, otpURL) 1859 | } 1860 | 1861 | // Check actions are included when present 1862 | if len(tt.actions) > 0 { 1863 | if _, exists := response["available_actions"]; !exists { 1864 | t.Error("Expected available_actions to be present in response") 1865 | } 1866 | } 1867 | 1868 | // Check next step instructions for OTP case 1869 | if tt.paymentID != "" && len(tt.actions) == 0 { 1870 | if _, exists := response["next_step"]; !exists { 1871 | t.Error("Expected next_step to be present for fallback case") 1872 | } 1873 | } 1874 | }) 1875 | } 1876 | } 1877 | 1878 | func Test_addNextStepInstructions(t *testing.T) { 1879 | tests := []struct { 1880 | name string 1881 | paymentID string 1882 | expected bool // whether next_step should be added 1883 | }{ 1884 | { 1885 | name: "valid payment ID", 1886 | paymentID: "pay_MT48CvBhIC98MQ", 1887 | expected: true, 1888 | }, 1889 | { 1890 | name: "empty payment ID", 1891 | paymentID: "", 1892 | expected: false, 1893 | }, 1894 | } 1895 | 1896 | for _, tt := range tests { 1897 | t.Run(tt.name, func(t *testing.T) { 1898 | response := make(map[string]interface{}) 1899 | addNextStepInstructions(response, tt.paymentID) 1900 | 1901 | if tt.expected { 1902 | if _, exists := response["next_step"]; !exists { 1903 | t.Error("Expected next_step to be added") 1904 | } 1905 | if _, exists := response["next_tool"]; !exists { 1906 | t.Error("Expected next_tool to be added") 1907 | } 1908 | if _, exists := response["next_tool_params"]; !exists { 1909 | t.Error("Expected next_tool_params to be added") 1910 | } 1911 | 1912 | // Check specific values 1913 | if response["next_tool"] != "resend_otp" { 1914 | t.Errorf("Expected next_tool to be 'resend_otp', got %v", 1915 | response["next_tool"]) 1916 | } 1917 | 1918 | params, ok := response["next_tool_params"].(map[string]interface{}) 1919 | if !ok || params["payment_id"] != tt.paymentID { 1920 | t.Errorf("Expected next_tool_params to contain payment_id %s", 1921 | tt.paymentID) 1922 | } 1923 | } else { 1924 | if _, exists := response["next_step"]; exists { 1925 | t.Error("Expected next_step NOT to be added for empty payment ID") 1926 | } 1927 | } 1928 | }) 1929 | } 1930 | } 1931 | 1932 | func Test_sendOtp_validation(t *testing.T) { 1933 | tests := []struct { 1934 | name string 1935 | otpURL string 1936 | expectedErr string 1937 | }{ 1938 | { 1939 | name: "empty URL", 1940 | otpURL: "", 1941 | expectedErr: "OTP URL is empty", 1942 | }, 1943 | { 1944 | name: "invalid URL", 1945 | otpURL: "not-a-valid-url", 1946 | expectedErr: "OTP URL must use HTTPS", 1947 | }, 1948 | { 1949 | name: "non-HTTPS URL", 1950 | otpURL: "http://api.razorpay.com/v1/payments/pay_123/otp_generate", 1951 | expectedErr: "OTP URL must use HTTPS", 1952 | }, 1953 | { 1954 | name: "non-Razorpay domain", 1955 | otpURL: "https://malicious.com/v1/payments/pay_123/otp_generate", 1956 | expectedErr: "OTP URL must be from Razorpay domain", 1957 | }, 1958 | { 1959 | name: "valid Razorpay URL - should fail at HTTP call", 1960 | otpURL: "https://api.razorpay.com/v1/payments/pay_123/otp_generate", 1961 | expectedErr: "OTP generation failed", 1962 | }, 1963 | } 1964 | 1965 | for _, tt := range tests { 1966 | t.Run(tt.name, func(t *testing.T) { 1967 | err := sendOtp(tt.otpURL) 1968 | if err == nil { 1969 | t.Error("Expected error but got nil") 1970 | return 1971 | } 1972 | 1973 | if tt.expectedErr != "" && !strings.Contains(err.Error(), tt.expectedErr) { 1974 | t.Errorf("Expected error to contain '%s', got '%s'", 1975 | tt.expectedErr, err.Error()) 1976 | } 1977 | }) 1978 | } 1979 | } 1980 | 1981 | func Test_extractOtpSubmitURL(t *testing.T) { 1982 | tests := []struct { 1983 | name string 1984 | payment map[string]interface{} 1985 | expected string 1986 | }{ 1987 | { 1988 | name: "payment with next actions containing otp_submit", 1989 | payment: map[string]interface{}{ 1990 | "next": []interface{}{ 1991 | map[string]interface{}{ 1992 | "action": "otp_submit", 1993 | "url": "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 1994 | }, 1995 | }, 1996 | }, 1997 | expected: "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 1998 | }, 1999 | { 2000 | name: "payment with multiple next actions", 2001 | payment: map[string]interface{}{ 2002 | "next": []interface{}{ 2003 | map[string]interface{}{ 2004 | "action": "redirect", 2005 | "url": "https://api.razorpay.com/v1/payments/pay_123/authenticate", 2006 | }, 2007 | map[string]interface{}{ 2008 | "action": "otp_submit", 2009 | "url": "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 2010 | }, 2011 | }, 2012 | }, 2013 | expected: "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 2014 | }, 2015 | { 2016 | name: "payment with no next actions", 2017 | payment: map[string]interface{}{ 2018 | "status": "captured", 2019 | }, 2020 | expected: "", 2021 | }, 2022 | { 2023 | name: "payment with next actions but no otp_submit", 2024 | payment: map[string]interface{}{ 2025 | "next": []interface{}{ 2026 | map[string]interface{}{ 2027 | "action": "redirect", 2028 | "url": "https://api.razorpay.com/v1/payments/pay_123/authenticate", 2029 | }, 2030 | }, 2031 | }, 2032 | expected: "", 2033 | }, 2034 | { 2035 | name: "payment with empty next array", 2036 | payment: map[string]interface{}{ 2037 | "next": []interface{}{}, 2038 | }, 2039 | expected: "", 2040 | }, 2041 | { 2042 | name: "payment with invalid next structure", 2043 | payment: map[string]interface{}{ 2044 | "next": "invalid_structure", 2045 | }, 2046 | expected: "", 2047 | }, 2048 | { 2049 | name: "payment with otp_submit action but nil URL", 2050 | payment: map[string]interface{}{ 2051 | "next": []interface{}{ 2052 | map[string]interface{}{ 2053 | "action": "otp_submit", 2054 | "url": nil, // nil URL should return empty string 2055 | }, 2056 | }, 2057 | }, 2058 | expected: "", 2059 | }, 2060 | { 2061 | name: "payment with otp_submit action but non-string URL", 2062 | payment: map[string]interface{}{ 2063 | "next": []interface{}{ 2064 | map[string]interface{}{ 2065 | "action": "otp_submit", 2066 | "url": 123, // non-string URL should cause type assertion to fail 2067 | }, 2068 | }, 2069 | }, 2070 | expected: "", 2071 | }, 2072 | { 2073 | name: "payment with otp_submit action but missing URL field", 2074 | payment: map[string]interface{}{ 2075 | "next": []interface{}{ 2076 | map[string]interface{}{ 2077 | "action": "otp_submit", 2078 | // no url field 2079 | }, 2080 | }, 2081 | }, 2082 | expected: "", 2083 | }, 2084 | { 2085 | name: "payment with mixed valid and invalid items in next array", 2086 | payment: map[string]interface{}{ 2087 | "next": []interface{}{ 2088 | "invalid_item", // This should be skipped 2089 | map[string]interface{}{ 2090 | "action": "redirect", 2091 | "url": "https://example.com/redirect", 2092 | }, 2093 | 123, // Another invalid item that should be skipped 2094 | map[string]interface{}{ 2095 | "action": "otp_submit", 2096 | "url": "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 2097 | }, 2098 | }, 2099 | }, 2100 | expected: "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 2101 | }, 2102 | { 2103 | name: "payment with otp_submit action but missing action field", 2104 | payment: map[string]interface{}{ 2105 | "next": []interface{}{ 2106 | map[string]interface{}{ 2107 | // no action field 2108 | "url": "https://api.razorpay.com/v1/payments/pay_123/otp/submit", 2109 | }, 2110 | }, 2111 | }, 2112 | expected: "", 2113 | }, 2114 | } 2115 | 2116 | for _, tt := range tests { 2117 | t.Run(tt.name, func(t *testing.T) { 2118 | result := extractOtpSubmitURL(tt.payment) 2119 | if result != tt.expected { 2120 | t.Errorf("extractOtpSubmitURL() = %v, want %v", result, tt.expected) 2121 | } 2122 | }) 2123 | } 2124 | } 2125 | 2126 | // Test_extractOtpSubmitURL_invalidInput tests the 2127 | // function with invalid input types 2128 | func Test_extractOtpSubmitURL_invalidInput(t *testing.T) { 2129 | tests := []struct { 2130 | name string 2131 | input interface{} 2132 | expected string 2133 | }{ 2134 | { 2135 | name: "nil input", 2136 | input: nil, 2137 | expected: "", 2138 | }, 2139 | { 2140 | name: "string input instead of map", 2141 | input: "invalid_input", 2142 | expected: "", 2143 | }, 2144 | { 2145 | name: "integer input instead of map", 2146 | input: 123, 2147 | expected: "", 2148 | }, 2149 | { 2150 | name: "slice input instead of map", 2151 | input: []string{"invalid"}, 2152 | expected: "", 2153 | }, 2154 | } 2155 | 2156 | for _, tt := range tests { 2157 | t.Run(tt.name, func(t *testing.T) { 2158 | result := extractOtpSubmitURL(tt.input) 2159 | if result != tt.expected { 2160 | t.Errorf("extractOtpSubmitURL() = %v, want %v", result, tt.expected) 2161 | } 2162 | }) 2163 | } 2164 | } 2165 | 2166 | func Test_ResendOtp(t *testing.T) { 2167 | resendOtpPathFmt := fmt.Sprintf( 2168 | "/%s%s/%%s/otp/resend", 2169 | constants.VERSION_V1, 2170 | constants.PAYMENT_URL, 2171 | ) 2172 | 2173 | successResendOtpResp := map[string]interface{}{ 2174 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 2175 | "next": []interface{}{ 2176 | map[string]interface{}{ 2177 | "action": "otp_submit", 2178 | "url": "https://api.razorpay.com/v1/payments/" + 2179 | "pay_MT48CvBhIC98MQ/otp/submit", 2180 | }, 2181 | }, 2182 | } 2183 | 2184 | paymentNotFoundResp := map[string]interface{}{ 2185 | "error": map[string]interface{}{ 2186 | "code": "BAD_REQUEST_ERROR", 2187 | "description": "Payment not found", 2188 | }, 2189 | } 2190 | 2191 | tests := []RazorpayToolTestCase{ 2192 | { 2193 | Name: "successful OTP resend", 2194 | Request: map[string]interface{}{ 2195 | "payment_id": "pay_MT48CvBhIC98MQ", 2196 | }, 2197 | MockHttpClient: func() (*http.Client, *httptest.Server) { 2198 | return mock.NewHTTPClient( 2199 | mock.Endpoint{ 2200 | Path: fmt.Sprintf(resendOtpPathFmt, "pay_MT48CvBhIC98MQ"), 2201 | Method: "POST", 2202 | Response: successResendOtpResp, 2203 | }, 2204 | ) 2205 | }, 2206 | ExpectError: false, 2207 | ExpectedResult: map[string]interface{}{ 2208 | "payment_id": "pay_MT48CvBhIC98MQ", 2209 | "status": "success", 2210 | "message": "OTP sent successfully. Please enter the OTP received on your " + 2211 | "mobile number to complete the payment.", 2212 | "next_step": "Use 'submit_otp' tool with the OTP code received " + 2213 | "from user to complete payment authentication.", 2214 | "next_tool": "submit_otp", 2215 | "next_tool_params": map[string]interface{}{ 2216 | "payment_id": "pay_MT48CvBhIC98MQ", 2217 | "otp_string": "{OTP_CODE_FROM_USER}", 2218 | }, 2219 | "otp_submit_url": "https://api.razorpay.com/v1/payments/" + 2220 | "pay_MT48CvBhIC98MQ/otp/submit", 2221 | "response_data": successResendOtpResp, 2222 | }, 2223 | }, 2224 | { 2225 | Name: "payment not found for OTP resend", 2226 | Request: map[string]interface{}{ 2227 | "payment_id": "pay_invalid", 2228 | }, 2229 | MockHttpClient: func() (*http.Client, *httptest.Server) { 2230 | return mock.NewHTTPClient( 2231 | mock.Endpoint{ 2232 | Path: fmt.Sprintf(resendOtpPathFmt, "pay_invalid"), 2233 | Method: "POST", 2234 | Response: paymentNotFoundResp, 2235 | }, 2236 | ) 2237 | }, 2238 | ExpectError: true, 2239 | ExpectedErrMsg: "OTP resend failed: Payment not found", 2240 | }, 2241 | { 2242 | Name: "missing payment_id parameter for resend", 2243 | Request: map[string]interface{}{ 2244 | // No payment_id provided 2245 | }, 2246 | MockHttpClient: nil, // No HTTP client needed for validation error 2247 | ExpectError: true, 2248 | ExpectedErrMsg: "missing required parameter: payment_id", 2249 | }, 2250 | { 2251 | Name: "OTP resend without next actions", 2252 | Request: map[string]interface{}{ 2253 | "payment_id": "pay_MT48CvBhIC98MQ", 2254 | }, 2255 | MockHttpClient: func() (*http.Client, *httptest.Server) { 2256 | return mock.NewHTTPClient( 2257 | mock.Endpoint{ 2258 | Path: fmt.Sprintf(resendOtpPathFmt, "pay_MT48CvBhIC98MQ"), 2259 | Method: "POST", 2260 | Response: map[string]interface{}{ 2261 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 2262 | "status": "created", 2263 | }, 2264 | }, 2265 | ) 2266 | }, 2267 | ExpectError: false, 2268 | ExpectedResult: map[string]interface{}{ 2269 | "payment_id": "pay_MT48CvBhIC98MQ", 2270 | "status": "success", 2271 | "message": "OTP sent successfully. Please enter the OTP received on your " + 2272 | "mobile number to complete the payment.", 2273 | "next_step": "Use 'submit_otp' tool with the OTP code received " + 2274 | "from user to complete payment authentication.", 2275 | "next_tool": "submit_otp", 2276 | "next_tool_params": map[string]interface{}{ 2277 | "payment_id": "pay_MT48CvBhIC98MQ", 2278 | "otp_string": "{OTP_CODE_FROM_USER}", 2279 | }, 2280 | "response_data": map[string]interface{}{ 2281 | "razorpay_payment_id": "pay_MT48CvBhIC98MQ", 2282 | "status": "created", 2283 | }, 2284 | }, 2285 | }, 2286 | } 2287 | 2288 | for _, tc := range tests { 2289 | t.Run(tc.Name, func(t *testing.T) { 2290 | runToolTest(t, tc, ResendOtp, "OTP Resend") 2291 | }) 2292 | } 2293 | } 2294 | 2295 | // Test_sendOtp_additionalCases tests additional cases for sendOtp function 2296 | func Test_sendOtp_additionalCases(t *testing.T) { 2297 | tests := []struct { 2298 | name string 2299 | otpURL string 2300 | expectedErr string 2301 | }{ 2302 | { 2303 | name: "URL with invalid characters", 2304 | otpURL: "https://api.razorpay.com/v1/payments/pay_123/" + 2305 | "otp_generate?param=value with spaces", 2306 | expectedErr: "OTP generation failed", 2307 | }, 2308 | { 2309 | name: "URL with special characters in domain", 2310 | otpURL: "https://api-test.razorpay.com/v1/payments/" + 2311 | "pay_123/otp_generate", 2312 | expectedErr: "OTP generation failed", 2313 | }, 2314 | } 2315 | 2316 | for _, tt := range tests { 2317 | t.Run(tt.name, func(t *testing.T) { 2318 | err := sendOtp(tt.otpURL) 2319 | if err == nil { 2320 | t.Error("Expected error but got nil") 2321 | return 2322 | } 2323 | if tt.expectedErr != "" && !strings.Contains(err.Error(), tt.expectedErr) { 2324 | t.Errorf( 2325 | "Expected error to contain '%s', got '%s'", tt.expectedErr, err.Error(), 2326 | ) 2327 | } 2328 | }) 2329 | } 2330 | } 2331 | 2332 | // Test_buildPaymentData_edgeCases tests edge cases for 2333 | // buildPaymentData function 2334 | func Test_buildPaymentData_edgeCases(t *testing.T) { 2335 | tests := []struct { 2336 | name string 2337 | params map[string]interface{} 2338 | currency string 2339 | customerID string 2340 | expectedError string 2341 | shouldContain map[string]interface{} 2342 | }{ 2343 | { 2344 | name: "payment data with valid customer ID", 2345 | params: map[string]interface{}{ 2346 | "amount": 10000, 2347 | "order_id": "order_123", 2348 | }, 2349 | currency: "INR", 2350 | customerID: "cust_123456789", 2351 | shouldContain: map[string]interface{}{ 2352 | "amount": 10000, 2353 | "currency": "INR", 2354 | "order_id": "order_123", 2355 | "customer_id": "cust_123456789", 2356 | }, 2357 | }, 2358 | { 2359 | name: "payment data with empty token", 2360 | params: map[string]interface{}{ 2361 | "amount": 10000, 2362 | "order_id": "order_123", 2363 | "token": "", // Empty token should not be added 2364 | }, 2365 | currency: "INR", 2366 | customerID: "cust_123456789", 2367 | shouldContain: map[string]interface{}{ 2368 | "amount": 10000, 2369 | "currency": "INR", 2370 | "order_id": "order_123", 2371 | "customer_id": "cust_123456789", 2372 | }, 2373 | }, 2374 | { 2375 | name: "payment data with empty customer ID", 2376 | params: map[string]interface{}{ 2377 | "amount": 10000, 2378 | "order_id": "order_123", 2379 | "token": "token_123", 2380 | }, 2381 | currency: "INR", 2382 | customerID: "", 2383 | shouldContain: map[string]interface{}{ 2384 | "amount": 10000, 2385 | "currency": "INR", 2386 | "order_id": "order_123", 2387 | "token": "token_123", 2388 | }, 2389 | }, 2390 | { 2391 | name: "payment data with all parameters", 2392 | params: map[string]interface{}{ 2393 | "amount": 10000, 2394 | "order_id": "order_123", 2395 | "token": "token_123", 2396 | "email": "[email protected]", 2397 | "contact": "9876543210", 2398 | "method": "upi", 2399 | "save": true, 2400 | "upi": map[string]interface{}{ 2401 | "flow": "collect", 2402 | "expiry_time": "6", 2403 | "vpa": "test@upi", 2404 | }, 2405 | }, 2406 | currency: "INR", 2407 | customerID: "cust_123456789", 2408 | shouldContain: map[string]interface{}{ 2409 | "amount": 10000, 2410 | "currency": "INR", 2411 | "order_id": "order_123", 2412 | "customer_id": "cust_123456789", 2413 | "token": "token_123", 2414 | "email": "[email protected]", 2415 | "contact": "9876543210", 2416 | "method": "upi", 2417 | "save": true, 2418 | }, 2419 | }, 2420 | } 2421 | 2422 | for _, tt := range tests { 2423 | t.Run(tt.name, func(t *testing.T) { 2424 | result := buildPaymentData(tt.params, tt.currency, tt.customerID) 2425 | 2426 | if result == nil { 2427 | t.Error("Expected result but got nil") 2428 | return 2429 | } 2430 | 2431 | // Check that all expected fields are present 2432 | for key, expectedValue := range tt.shouldContain { 2433 | actualValue, exists := (*result)[key] 2434 | if !exists { 2435 | t.Errorf("Expected key '%s' to exist in result", key) 2436 | } else if actualValue != expectedValue { 2437 | t.Errorf( 2438 | "Expected '%s' to be %v, got %v", key, expectedValue, actualValue, 2439 | ) 2440 | } 2441 | } 2442 | 2443 | // Check that empty token is not added 2444 | if tt.params["token"] == "" { 2445 | if _, exists := (*result)["token"]; exists { 2446 | t.Error("Empty token should not be added to payment data") 2447 | } 2448 | } 2449 | 2450 | // Check that customer_id is not added when customerID is empty 2451 | if tt.customerID == "" { 2452 | if _, exists := (*result)["customer_id"]; exists { 2453 | t.Error("customer_id should not be added when customerID is empty") 2454 | } 2455 | } 2456 | 2457 | }) 2458 | } 2459 | } 2460 | 2461 | // Test_createOrGetCustomer_scenarios tests 2462 | // createOrGetCustomer function scenarios 2463 | func Test_createOrGetCustomer_scenarios(t *testing.T) { 2464 | tests := []struct { 2465 | name string 2466 | params map[string]interface{} 2467 | mockSetup func() (*http.Client, *httptest.Server) 2468 | expectedError string 2469 | expectedResult map[string]interface{} 2470 | }{ 2471 | { 2472 | name: "successful customer creation with contact", 2473 | params: map[string]interface{}{ 2474 | "contact": "9876543210", 2475 | }, 2476 | mockSetup: func() (*http.Client, *httptest.Server) { 2477 | return mock.NewHTTPClient( 2478 | mock.Endpoint{ 2479 | Path: "/v1/customers", 2480 | Method: "POST", 2481 | Response: map[string]interface{}{ 2482 | "id": "cust_123456789", 2483 | "contact": "9876543210", 2484 | "email": "[email protected]", 2485 | }, 2486 | }, 2487 | ) 2488 | }, 2489 | expectedResult: map[string]interface{}{ 2490 | "id": "cust_123456789", 2491 | "contact": "9876543210", 2492 | "email": "[email protected]", 2493 | }, 2494 | }, 2495 | { 2496 | name: "no contact provided - returns nil", 2497 | params: map[string]interface{}{ 2498 | "amount": 10000, 2499 | }, 2500 | mockSetup: func() (*http.Client, *httptest.Server) { 2501 | return mock.NewHTTPClient() 2502 | }, 2503 | expectedResult: nil, 2504 | }, 2505 | { 2506 | name: "empty contact provided - returns nil", 2507 | params: map[string]interface{}{ 2508 | "contact": "", 2509 | }, 2510 | mockSetup: func() (*http.Client, *httptest.Server) { 2511 | return mock.NewHTTPClient() 2512 | }, 2513 | expectedResult: nil, 2514 | }, 2515 | { 2516 | name: "customer creation API error", 2517 | params: map[string]interface{}{ 2518 | "contact": "9876543210", 2519 | }, 2520 | mockSetup: func() (*http.Client, *httptest.Server) { 2521 | return mock.NewHTTPClient( 2522 | mock.Endpoint{ 2523 | Path: "/v1/customers", 2524 | Method: "POST", 2525 | Response: map[string]interface{}{ 2526 | "error": map[string]interface{}{ 2527 | "code": "BAD_REQUEST_ERROR", 2528 | "description": "Invalid contact number", 2529 | }, 2530 | }, 2531 | }, 2532 | ) 2533 | }, 2534 | expectedError: "failed to create/fetch customer with contact 9876543210", 2535 | }, 2536 | } 2537 | 2538 | for _, tt := range tests { 2539 | t.Run(tt.name, func(t *testing.T) { 2540 | client, server := newMockRzpClient(tt.mockSetup) 2541 | if server != nil { 2542 | defer server.Close() 2543 | } 2544 | 2545 | result, err := createOrGetCustomer(client, tt.params) 2546 | 2547 | if tt.expectedError != "" { 2548 | if err == nil { 2549 | t.Error("Expected error but got nil") 2550 | return 2551 | } 2552 | if !strings.Contains(err.Error(), tt.expectedError) { 2553 | t.Errorf( 2554 | "Expected error to contain '%s', got '%s'", tt.expectedError, err.Error(), 2555 | ) 2556 | } 2557 | } else { 2558 | if err != nil { 2559 | t.Errorf("Unexpected error: %v", err) 2560 | return 2561 | } 2562 | 2563 | switch { 2564 | case tt.expectedResult == nil && result != nil: 2565 | t.Errorf("Expected nil result but got %v", result) 2566 | case tt.expectedResult != nil && result == nil: 2567 | t.Error("Expected result but got nil") 2568 | case tt.expectedResult != nil && result != nil: 2569 | if result["id"] != tt.expectedResult["id"] { 2570 | t.Errorf( 2571 | "Expected customer ID '%s', got '%s'", tt.expectedResult["id"], 2572 | result["id"], 2573 | ) 2574 | } 2575 | } 2576 | } 2577 | }) 2578 | } 2579 | } 2580 | 2581 | // Test_processVPAParameters_scenarios tests 2582 | // 2583 | // processUPIParameters function scenarios 2584 | func Test_processUPIParameters_scenarios(t *testing.T) { 2585 | tests := []struct { 2586 | name string 2587 | inputParams map[string]interface{} 2588 | expectedParams map[string]interface{} 2589 | }{ 2590 | { 2591 | name: "VPA parameter provided - sets UPI parameters", 2592 | inputParams: map[string]interface{}{ 2593 | "amount": 10000, 2594 | "order_id": "order_123", 2595 | "vpa": "9876543210@paytm", 2596 | }, 2597 | expectedParams: map[string]interface{}{ 2598 | "amount": 10000, 2599 | "order_id": "order_123", 2600 | "vpa": "9876543210@paytm", 2601 | "method": "upi", 2602 | "upi": map[string]interface{}{ 2603 | "flow": "collect", 2604 | "expiry_time": "6", 2605 | "vpa": "9876543210@paytm", 2606 | }, 2607 | }, 2608 | }, 2609 | { 2610 | name: "empty VPA parameter - no changes", 2611 | inputParams: map[string]interface{}{ 2612 | "amount": 10000, 2613 | "order_id": "order_123", 2614 | "vpa": "", 2615 | }, 2616 | expectedParams: map[string]interface{}{ 2617 | "amount": 10000, 2618 | "order_id": "order_123", 2619 | "vpa": "", 2620 | }, 2621 | }, 2622 | { 2623 | name: "no VPA parameter - no changes", 2624 | inputParams: map[string]interface{}{ 2625 | "amount": 10000, 2626 | "order_id": "order_123", 2627 | }, 2628 | expectedParams: map[string]interface{}{ 2629 | "amount": 10000, 2630 | "order_id": "order_123", 2631 | }, 2632 | }, 2633 | { 2634 | name: "UPI intent parameter provided - sets UPI intent parameters", 2635 | inputParams: map[string]interface{}{ 2636 | "amount": 15000, 2637 | "order_id": "order_456", 2638 | "upi_intent": true, 2639 | }, 2640 | expectedParams: map[string]interface{}{ 2641 | "amount": 15000, 2642 | "order_id": "order_456", 2643 | "upi_intent": true, 2644 | "method": "upi", 2645 | "upi": map[string]interface{}{ 2646 | "flow": "intent", 2647 | }, 2648 | }, 2649 | }, 2650 | { 2651 | name: "UPI intent false - no changes", 2652 | inputParams: map[string]interface{}{ 2653 | "amount": 10000, 2654 | "order_id": "order_123", 2655 | "upi_intent": false, 2656 | }, 2657 | expectedParams: map[string]interface{}{ 2658 | "amount": 10000, 2659 | "order_id": "order_123", 2660 | "upi_intent": false, 2661 | }, 2662 | }, 2663 | { 2664 | name: "both VPA and UPI intent provided - UPI intent takes precedence", 2665 | inputParams: map[string]interface{}{ 2666 | "amount": 20000, 2667 | "order_id": "order_789", 2668 | "vpa": "test@upi", 2669 | "upi_intent": true, 2670 | }, 2671 | expectedParams: map[string]interface{}{ 2672 | "amount": 20000, 2673 | "order_id": "order_789", 2674 | "vpa": "test@upi", 2675 | "upi_intent": true, 2676 | "method": "upi", 2677 | "upi": map[string]interface{}{ 2678 | "flow": "intent", 2679 | }, 2680 | }, 2681 | }, 2682 | } 2683 | 2684 | for _, tt := range tests { 2685 | t.Run(tt.name, func(t *testing.T) { 2686 | params := make(map[string]interface{}) 2687 | for k, v := range tt.inputParams { 2688 | params[k] = v 2689 | } 2690 | 2691 | processUPIParameters(params) 2692 | 2693 | for key, expectedValue := range tt.expectedParams { 2694 | actualValue, exists := params[key] 2695 | if !exists { 2696 | t.Errorf("Expected key '%s' to exist in params", key) 2697 | continue 2698 | } 2699 | 2700 | if key == "upi" { 2701 | expectedUPI := expectedValue.(map[string]interface{}) 2702 | actualUPI, ok := actualValue.(map[string]interface{}) 2703 | if !ok { 2704 | t.Errorf("Expected UPI to be map[string]interface{}, got %T", actualValue) 2705 | continue 2706 | } 2707 | for upiKey, upiValue := range expectedUPI { 2708 | if actualUPI[upiKey] != upiValue { 2709 | t.Errorf( 2710 | "Expected UPI[%s] to be '%v', got '%v'", 2711 | upiKey, upiValue, actualUPI[upiKey], 2712 | ) 2713 | } 2714 | } 2715 | } else if actualValue != expectedValue { 2716 | t.Errorf( 2717 | "Expected '%s' to be '%v', got '%v'", key, expectedValue, actualValue, 2718 | ) 2719 | } 2720 | } 2721 | }) 2722 | } 2723 | } 2724 | 2725 | // Test_addAdditionalPaymentParameters_scenarios 2726 | // tests addAdditionalPaymentParameters function scenarios 2727 | // Note: method parameter is set internally by VPA processing, not by user input 2728 | func Test_addAdditionalPaymentParameters_scenarios(t *testing.T) { 2729 | tests := []struct { 2730 | name string 2731 | paymentData map[string]interface{} 2732 | params map[string]interface{} 2733 | expectedResult map[string]interface{} 2734 | }{ 2735 | { 2736 | name: "all additional parameters provided (method set internally)", 2737 | paymentData: map[string]interface{}{ 2738 | "amount": 10000, 2739 | "currency": "INR", 2740 | }, 2741 | params: map[string]interface{}{ 2742 | "method": "upi", 2743 | "save": true, 2744 | "upi": map[string]interface{}{ 2745 | "flow": "collect", 2746 | "vpa": "test@upi", 2747 | }, 2748 | }, 2749 | expectedResult: map[string]interface{}{ 2750 | "amount": 10000, 2751 | "currency": "INR", 2752 | "method": "upi", 2753 | "save": true, 2754 | "upi": map[string]interface{}{ 2755 | "flow": "collect", 2756 | "vpa": "test@upi", 2757 | }, 2758 | }, 2759 | }, 2760 | { 2761 | name: "empty method parameter - not added (internal processing)", 2762 | paymentData: map[string]interface{}{ 2763 | "amount": 10000, 2764 | }, 2765 | params: map[string]interface{}{ 2766 | "method": "", 2767 | "save": false, 2768 | }, 2769 | expectedResult: map[string]interface{}{ 2770 | "amount": 10000, 2771 | "save": false, 2772 | }, 2773 | }, 2774 | { 2775 | name: "nil UPI parameters - not added (method set internally)", 2776 | paymentData: map[string]interface{}{ 2777 | "amount": 10000, 2778 | }, 2779 | params: map[string]interface{}{ 2780 | "method": "card", 2781 | "upi": nil, 2782 | }, 2783 | expectedResult: map[string]interface{}{ 2784 | "amount": 10000, 2785 | "method": "card", 2786 | }, 2787 | }, 2788 | { 2789 | name: "invalid UPI parameter type - not added (method set internally)", 2790 | paymentData: map[string]interface{}{ 2791 | "amount": 10000, 2792 | }, 2793 | params: map[string]interface{}{ 2794 | "method": "upi", 2795 | "upi": "invalid_type", 2796 | }, 2797 | expectedResult: map[string]interface{}{ 2798 | "amount": 10000, 2799 | "method": "upi", 2800 | }, 2801 | }, 2802 | { 2803 | name: "recurring parameter provided", 2804 | paymentData: map[string]interface{}{ 2805 | "amount": 10000, 2806 | }, 2807 | params: map[string]interface{}{ 2808 | "recurring": true, 2809 | }, 2810 | expectedResult: map[string]interface{}{ 2811 | "amount": 10000, 2812 | "recurring": true, 2813 | }, 2814 | }, 2815 | { 2816 | name: "recurring parameter false", 2817 | paymentData: map[string]interface{}{ 2818 | "amount": 10000, 2819 | }, 2820 | params: map[string]interface{}{ 2821 | "recurring": false, 2822 | }, 2823 | expectedResult: map[string]interface{}{ 2824 | "amount": 10000, 2825 | "recurring": false, 2826 | }, 2827 | }, 2828 | } 2829 | 2830 | for _, tt := range tests { 2831 | t.Run(tt.name, func(t *testing.T) { 2832 | paymentData := make(map[string]interface{}) 2833 | for k, v := range tt.paymentData { 2834 | paymentData[k] = v 2835 | } 2836 | 2837 | addAdditionalPaymentParameters(paymentData, tt.params) 2838 | 2839 | for key, expectedValue := range tt.expectedResult { 2840 | actualValue, exists := paymentData[key] 2841 | if !exists { 2842 | t.Errorf("Expected key '%s' to exist in paymentData", key) 2843 | continue 2844 | } 2845 | 2846 | if key == "upi" { 2847 | expectedUPI := expectedValue.(map[string]interface{}) 2848 | actualUPI, ok := actualValue.(map[string]interface{}) 2849 | if !ok { 2850 | t.Errorf("Expected UPI to be map[string]interface{}, got %T", actualValue) 2851 | continue 2852 | } 2853 | for upiKey, upiValue := range expectedUPI { 2854 | if actualUPI[upiKey] != upiValue { 2855 | t.Errorf( 2856 | "Expected UPI[%s] to be '%v', got '%v'", upiKey, upiValue, 2857 | actualUPI[upiKey], 2858 | ) 2859 | } 2860 | } 2861 | } else if actualValue != expectedValue { 2862 | t.Errorf( 2863 | "Expected '%s' to be '%v', got '%v'", key, expectedValue, actualValue, 2864 | ) 2865 | } 2866 | } 2867 | 2868 | // Check that no unexpected keys were added 2869 | for key := range paymentData { 2870 | if _, expected := tt.expectedResult[key]; !expected { 2871 | t.Errorf("Unexpected key '%s' found in paymentData", key) 2872 | } 2873 | } 2874 | }) 2875 | } 2876 | } 2877 | 2878 | // Test_addContactAndEmailToPaymentData_scenarios 2879 | // tests addContactAndEmailToPaymentData function scenarios 2880 | func Test_addContactAndEmailToPaymentData_scenarios(t *testing.T) { 2881 | tests := []struct { 2882 | name string 2883 | paymentData map[string]interface{} 2884 | params map[string]interface{} 2885 | expectedResult map[string]interface{} 2886 | }{ 2887 | { 2888 | name: "both contact and email provided", 2889 | paymentData: map[string]interface{}{ 2890 | "amount": 10000, 2891 | }, 2892 | params: map[string]interface{}{ 2893 | "contact": "9876543210", 2894 | "email": "[email protected]", 2895 | }, 2896 | expectedResult: map[string]interface{}{ 2897 | "amount": 10000, 2898 | "contact": "9876543210", 2899 | "email": "[email protected]", 2900 | }, 2901 | }, 2902 | { 2903 | name: "only contact provided - email generated", 2904 | paymentData: map[string]interface{}{ 2905 | "amount": 10000, 2906 | }, 2907 | params: map[string]interface{}{ 2908 | "contact": "9876543210", 2909 | }, 2910 | expectedResult: map[string]interface{}{ 2911 | "amount": 10000, 2912 | "contact": "9876543210", 2913 | "email": "[email protected]", 2914 | }, 2915 | }, 2916 | { 2917 | name: "empty contact and email - nothing added", 2918 | paymentData: map[string]interface{}{ 2919 | "amount": 10000, 2920 | }, 2921 | params: map[string]interface{}{ 2922 | "contact": "", 2923 | "email": "", 2924 | }, 2925 | expectedResult: map[string]interface{}{ 2926 | "amount": 10000, 2927 | }, 2928 | }, 2929 | { 2930 | name: "contact provided but email is empty - email generated", 2931 | paymentData: map[string]interface{}{ 2932 | "amount": 10000, 2933 | }, 2934 | params: map[string]interface{}{ 2935 | "contact": "9876543210", 2936 | "email": "", 2937 | }, 2938 | expectedResult: map[string]interface{}{ 2939 | "amount": 10000, 2940 | "contact": "9876543210", 2941 | "email": "[email protected]", 2942 | }, 2943 | }, 2944 | } 2945 | 2946 | for _, tt := range tests { 2947 | t.Run(tt.name, func(t *testing.T) { 2948 | paymentData := make(map[string]interface{}) 2949 | for k, v := range tt.paymentData { 2950 | paymentData[k] = v 2951 | } 2952 | 2953 | addContactAndEmailToPaymentData(paymentData, tt.params) 2954 | 2955 | for key, expectedValue := range tt.expectedResult { 2956 | actualValue, exists := paymentData[key] 2957 | if !exists { 2958 | t.Errorf("Expected key '%s' to exist in paymentData", key) 2959 | continue 2960 | } 2961 | if actualValue != expectedValue { 2962 | t.Errorf( 2963 | "Expected '%s' to be '%v', got '%v'", key, expectedValue, actualValue, 2964 | ) 2965 | } 2966 | } 2967 | 2968 | // Check that no unexpected keys were added 2969 | for key := range paymentData { 2970 | if _, expected := tt.expectedResult[key]; !expected { 2971 | t.Errorf("Unexpected key '%s' found in paymentData", key) 2972 | } 2973 | } 2974 | }) 2975 | } 2976 | } 2977 | 2978 | // Test_processPaymentResult_edgeCases 2979 | // tests edge cases for processPaymentResult function 2980 | func Test_processPaymentResult_edgeCases(t *testing.T) { 2981 | tests := []struct { 2982 | name string 2983 | payment map[string]interface{} 2984 | expectedError string 2985 | shouldProcess bool 2986 | }{ 2987 | { 2988 | name: "payment with OTP URL that causes sendOtp to fail", 2989 | payment: map[string]interface{}{ 2990 | "razorpay_payment_id": "pay_123456789", 2991 | "next": []interface{}{ 2992 | map[string]interface{}{ 2993 | "action": "otp_generate", 2994 | "url": "http://invalid-url", // Invalid URL 2995 | }, 2996 | }, 2997 | }, 2998 | expectedError: "OTP generation failed", 2999 | }, 3000 | { 3001 | name: "payment with empty OTP URL", 3002 | payment: map[string]interface{}{ 3003 | "razorpay_payment_id": "pay_123456789", 3004 | "next": []interface{}{ 3005 | map[string]interface{}{ 3006 | "action": "otp_generate", 3007 | "url": "", // Empty URL should not trigger sendOtp 3008 | }, 3009 | }, 3010 | }, 3011 | shouldProcess: true, 3012 | }, 3013 | { 3014 | name: "payment without next actions", 3015 | payment: map[string]interface{}{ 3016 | "razorpay_payment_id": "pay_123456789", 3017 | }, 3018 | shouldProcess: true, 3019 | }, 3020 | } 3021 | 3022 | for _, tt := range tests { 3023 | t.Run(tt.name, func(t *testing.T) { 3024 | result, err := processPaymentResult(tt.payment) 3025 | 3026 | if tt.expectedError != "" { 3027 | if err == nil { 3028 | t.Error("Expected error but got nil") 3029 | return 3030 | } 3031 | if !strings.Contains(err.Error(), tt.expectedError) { 3032 | t.Errorf( 3033 | "Expected error to contain '%s', got '%s'", tt.expectedError, err.Error()) 3034 | } 3035 | } else if tt.shouldProcess { 3036 | if err != nil { 3037 | t.Errorf("Unexpected error: %v", err) 3038 | } 3039 | if result == nil { 3040 | t.Error("Expected result but got nil") 3041 | } else { 3042 | // Verify the response structure 3043 | if paymentID, exists := result["razorpay_payment_id"]; !exists || 3044 | paymentID == "" { 3045 | t.Error("Expected razorpay_payment_id in result") 3046 | } 3047 | if status, exists := result["status"]; !exists || 3048 | status != "payment_initiated" { 3049 | t.Error("Expected status to be 'payment_initiated'") 3050 | } 3051 | } 3052 | } 3053 | }) 3054 | } 3055 | } 3056 | 3057 | // Test for sendOtp function - comprehensive coverage 3058 | func TestSendOtp(t *testing.T) { 3059 | t.Run("empty OTP URL", func(t *testing.T) { 3060 | err := sendOtp("") 3061 | if err == nil { 3062 | t.Error("Expected error for empty OTP URL") 3063 | } 3064 | if err.Error() != "OTP URL is empty" { 3065 | t.Errorf("Expected 'OTP URL is empty', got '%s'", err.Error()) 3066 | } 3067 | }) 3068 | 3069 | t.Run("invalid URL format", func(t *testing.T) { 3070 | err := sendOtp("invalid-url") 3071 | if err == nil { 3072 | t.Error("Expected error for invalid URL") 3073 | } 3074 | // The URL parsing succeeds but fails on HTTPS check 3075 | if !strings.Contains(err.Error(), "OTP URL must use HTTPS") { 3076 | t.Errorf("Expected 'OTP URL must use HTTPS' error, got '%s'", err.Error()) 3077 | } 3078 | }) 3079 | 3080 | t.Run("non-HTTPS URL", func(t *testing.T) { 3081 | err := sendOtp("http://api.razorpay.com/v1/payments/otp") 3082 | if err == nil { 3083 | t.Error("Expected error for non-HTTPS URL") 3084 | } 3085 | if err.Error() != "OTP URL must use HTTPS" { 3086 | t.Errorf("Expected 'OTP URL must use HTTPS', got '%s'", err.Error()) 3087 | } 3088 | }) 3089 | 3090 | t.Run("non-Razorpay domain", func(t *testing.T) { 3091 | err := sendOtp("https://example.com/otp") 3092 | if err == nil { 3093 | t.Error("Expected error for non-Razorpay domain") 3094 | } 3095 | if err.Error() != "OTP URL must be from Razorpay domain" { 3096 | t.Errorf("Expected 'OTP URL must be from Razorpay domain', got '%s'", 3097 | err.Error()) 3098 | } 3099 | }) 3100 | 3101 | t.Run("successful OTP request", func(t *testing.T) { 3102 | // Since we can't actually call external APIs in tests, we'll test the 3103 | // validation logic by testing with a URL that would fail at HTTP call stage 3104 | err := sendOtp( 3105 | "https://api.razorpay.com/v1/payments/invalid-endpoint-for-test") 3106 | if err == nil { 3107 | t.Error("Expected error for invalid endpoint") 3108 | } 3109 | // This should fail at the HTTP request stage, which is expected 3110 | if !strings.Contains(err.Error(), "OTP generation failed") { 3111 | t.Logf("Got expected error: %s", err.Error()) 3112 | } 3113 | }) 3114 | 3115 | t.Run("HTTP request creation failure", func(t *testing.T) { 3116 | // Test with invalid characters that would cause http.NewRequest to fail 3117 | // This is difficult to trigger in practice, so we'll test URL validation 3118 | err := sendOtp("https://api.razorpay.com/v1/payments\x00/otp") 3119 | if err == nil { 3120 | t.Error("Expected error for invalid URL characters") 3121 | } 3122 | }) 3123 | } 3124 | 3125 | // Test for extractPaymentID function 3126 | func TestExtractPaymentID(t *testing.T) { 3127 | t.Run("payment ID exists", func(t *testing.T) { 3128 | payment := map[string]interface{}{ 3129 | "razorpay_payment_id": "pay_test123", 3130 | "other_field": "value", 3131 | } 3132 | result := extractPaymentID(payment) 3133 | if result != "pay_test123" { 3134 | t.Errorf("Expected 'pay_test123', got '%s'", result) 3135 | } 3136 | }) 3137 | 3138 | t.Run("payment ID missing", func(t *testing.T) { 3139 | payment := map[string]interface{}{ 3140 | "other_field": "value", 3141 | } 3142 | result := extractPaymentID(payment) 3143 | if result != "" { 3144 | t.Errorf("Expected empty string, got '%s'", result) 3145 | } 3146 | }) 3147 | 3148 | t.Run("payment ID is nil", func(t *testing.T) { 3149 | payment := map[string]interface{}{ 3150 | "razorpay_payment_id": nil, 3151 | } 3152 | result := extractPaymentID(payment) 3153 | if result != "" { 3154 | t.Errorf("Expected empty string, got '%s'", result) 3155 | } 3156 | }) 3157 | } 3158 | 3159 | // Test for extractNextActions function 3160 | func TestExtractNextActions(t *testing.T) { 3161 | t.Run("next actions exist", func(t *testing.T) { 3162 | payment := map[string]interface{}{ 3163 | "next": []interface{}{ 3164 | map[string]interface{}{ 3165 | "action": "redirect", 3166 | "url": "https://example.com", 3167 | }, 3168 | map[string]interface{}{ 3169 | "action": "otp", 3170 | "url": "https://otp.example.com", 3171 | }, 3172 | }, 3173 | } 3174 | result := extractNextActions(payment) 3175 | if len(result) != 2 { 3176 | t.Errorf("Expected 2 actions, got %d", len(result)) 3177 | } 3178 | if result[0]["action"] != "redirect" { 3179 | t.Errorf("Expected first action to be 'redirect', got '%s'", 3180 | result[0]["action"]) 3181 | } 3182 | }) 3183 | 3184 | t.Run("next field missing", func(t *testing.T) { 3185 | payment := map[string]interface{}{ 3186 | "other_field": "value", 3187 | } 3188 | result := extractNextActions(payment) 3189 | if len(result) != 0 { 3190 | t.Errorf("Expected empty slice, got %d actions", len(result)) 3191 | } 3192 | }) 3193 | 3194 | t.Run("next field is nil", func(t *testing.T) { 3195 | payment := map[string]interface{}{ 3196 | "next": nil, 3197 | } 3198 | result := extractNextActions(payment) 3199 | if len(result) != 0 { 3200 | t.Errorf("Expected empty slice, got %d actions", len(result)) 3201 | } 3202 | }) 3203 | 3204 | t.Run("next field is not a slice", func(t *testing.T) { 3205 | payment := map[string]interface{}{ 3206 | "next": "invalid_type", 3207 | } 3208 | result := extractNextActions(payment) 3209 | if len(result) != 0 { 3210 | t.Errorf("Expected empty slice, got %d actions", len(result)) 3211 | } 3212 | }) 3213 | 3214 | t.Run("next field contains non-map items", func(t *testing.T) { 3215 | payment := map[string]interface{}{ 3216 | "next": []interface{}{ 3217 | "invalid_item", 3218 | map[string]interface{}{ 3219 | "action": "valid_action", 3220 | }, 3221 | }, 3222 | } 3223 | result := extractNextActions(payment) 3224 | if len(result) != 1 { 3225 | t.Errorf("Expected 1 valid action, got %d", len(result)) 3226 | } 3227 | if result[0]["action"] != "valid_action" { 3228 | t.Errorf("Expected action to be 'valid_action', got '%s'", 3229 | result[0]["action"]) 3230 | } 3231 | }) 3232 | } 3233 | 3234 | // Test for addNextStepInstructions function 3235 | func TestAddNextStepInstructions(t *testing.T) { 3236 | t.Run("add next step instructions with payment ID", func(t *testing.T) { 3237 | result := make(map[string]interface{}) 3238 | paymentID := "pay_test123" 3239 | 3240 | addNextStepInstructions(result, paymentID) 3241 | 3242 | nextStep, exists := result["next_step"] 3243 | if !exists { 3244 | t.Error("Expected next_step to be added") 3245 | } 3246 | 3247 | nextStepStr, ok := nextStep.(string) 3248 | if !ok { 3249 | t.Error("Expected next_step to be a string") 3250 | } 3251 | 3252 | if !strings.Contains(nextStepStr, "resend_otp") { 3253 | t.Error("Expected instructions to contain 'resend_otp'") 3254 | } 3255 | if !strings.Contains(nextStepStr, "submit_otp") { 3256 | t.Error("Expected instructions to contain 'submit_otp'") 3257 | } 3258 | 3259 | // Check next_tool 3260 | nextTool, exists := result["next_tool"] 3261 | if !exists { 3262 | t.Error("Expected next_tool to be added") 3263 | } 3264 | if nextTool != "resend_otp" { 3265 | t.Errorf("Expected next_tool to be 'resend_otp', got '%s'", nextTool) 3266 | } 3267 | 3268 | // Check next_tool_params 3269 | params, exists := result["next_tool_params"] 3270 | if !exists { 3271 | t.Error("Expected next_tool_params to be added") 3272 | } 3273 | paramsMap, ok := params.(map[string]interface{}) 3274 | if !ok { 3275 | t.Error("Expected next_tool_params to be a map") 3276 | } 3277 | if paramsMap["payment_id"] != paymentID { 3278 | t.Errorf("Expected payment_id to be '%s', got '%s'", 3279 | paymentID, paramsMap["payment_id"]) 3280 | } 3281 | }) 3282 | 3283 | t.Run("empty payment ID", func(t *testing.T) { 3284 | result := make(map[string]interface{}) 3285 | 3286 | addNextStepInstructions(result, "") 3287 | 3288 | // Should not add anything when payment ID is empty 3289 | if len(result) != 0 { 3290 | t.Error("Expected no fields to be added for empty payment ID") 3291 | } 3292 | }) 3293 | } 3294 | 3295 | // Test for processUPIParameters function 3296 | func TestProcessUPIParameters(t *testing.T) { 3297 | t.Run("processUPIParameters", func(t *testing.T) { 3298 | // Test with VPA 3299 | params := map[string]interface{}{ 3300 | "vpa": "test@upi", 3301 | } 3302 | processUPIParameters(params) 3303 | 3304 | if params["method"] != "upi" { 3305 | t.Errorf("Expected method to be 'upi', got '%s'", params["method"]) 3306 | } 3307 | 3308 | upi, exists := params["upi"] 3309 | if !exists { 3310 | t.Error("Expected upi field to be added") 3311 | } 3312 | 3313 | upiMap, ok := upi.(map[string]interface{}) 3314 | if !ok { 3315 | t.Error("Expected upi to be a map") 3316 | } 3317 | 3318 | if upiMap["vpa"] != "test@upi" { 3319 | t.Errorf("Expected vpa to be 'test@upi', got '%s'", upiMap["vpa"]) 3320 | } 3321 | if upiMap["flow"] != "collect" { 3322 | t.Errorf("Expected flow to be 'collect', got '%s'", upiMap["flow"]) 3323 | } 3324 | }) 3325 | 3326 | t.Run("processUPIParameters - UPI intent", func(t *testing.T) { 3327 | // Test with UPI intent 3328 | params := map[string]interface{}{ 3329 | "upi_intent": true, 3330 | } 3331 | processUPIParameters(params) 3332 | 3333 | if params["method"] != "upi" { 3334 | t.Errorf("Expected method to be 'upi', got '%s'", params["method"]) 3335 | } 3336 | 3337 | upi, exists := params["upi"] 3338 | if !exists { 3339 | t.Error("Expected upi field to be added") 3340 | } 3341 | 3342 | upiMap, ok := upi.(map[string]interface{}) 3343 | if !ok { 3344 | t.Error("Expected upi to be a map") 3345 | } 3346 | 3347 | if upiMap["flow"] != "intent" { 3348 | t.Errorf("Expected flow to be 'intent', got '%s'", upiMap["flow"]) 3349 | } 3350 | }) 3351 | 3352 | t.Run("processUPIParameters - no UPI params", func(t *testing.T) { 3353 | // Test with no UPI parameters 3354 | params := map[string]interface{}{ 3355 | "amount": 1000, 3356 | } 3357 | processUPIParameters(params) 3358 | 3359 | // Should not modify params when no UPI parameters are present 3360 | if _, exists := params["method"]; exists { 3361 | t.Error("Expected method not to be added when no UPI params") 3362 | } 3363 | if _, exists := params["upi"]; exists { 3364 | t.Error("Expected upi not to be added when no UPI params") 3365 | } 3366 | }) 3367 | 3368 | } 3369 | 3370 | // Test for createOrGetCustomer function 3371 | func TestCreateOrGetCustomer(t *testing.T) { 3372 | t.Run("createOrGetCustomer - no contact", func(t *testing.T) { 3373 | // Test with no contact parameter 3374 | params := map[string]interface{}{ 3375 | "amount": 1000, 3376 | } 3377 | 3378 | // This should return nil, nil since no contact is provided 3379 | result, err := createOrGetCustomer(nil, params) 3380 | 3381 | if result != nil { 3382 | t.Error("Expected nil result when no contact provided") 3383 | } 3384 | if err != nil { 3385 | t.Errorf("Expected no error when no contact provided, got %v", err) 3386 | } 3387 | }) 3388 | } 3389 | 3390 | // Test for buildPaymentData function 3391 | func TestBuildPaymentData(t *testing.T) { 3392 | t.Run("buildPaymentData", func(t *testing.T) { 3393 | params := map[string]interface{}{ 3394 | "amount": 1000, 3395 | "order_id": "order_test123", 3396 | } 3397 | currency := "INR" 3398 | customerId := "cust_test123" 3399 | 3400 | result := buildPaymentData(params, currency, customerId) 3401 | 3402 | if (*result)["amount"] != 1000 { 3403 | t.Errorf("Expected amount to be 1000, got %v", (*result)["amount"]) 3404 | } 3405 | if (*result)["currency"] != "INR" { 3406 | t.Errorf("Expected currency to be 'INR', got '%s'", (*result)["currency"]) 3407 | } 3408 | if (*result)["customer_id"] != customerId { 3409 | t.Errorf("Expected customer_id to be '%s', got '%s'", 3410 | customerId, (*result)["customer_id"]) 3411 | } 3412 | }) 3413 | 3414 | t.Run("buildPaymentData - no customer ID", func(t *testing.T) { 3415 | params := map[string]interface{}{ 3416 | "amount": 1000, 3417 | "order_id": "order_test123", 3418 | } 3419 | currency := "INR" 3420 | customerId := "" 3421 | 3422 | result := buildPaymentData(params, currency, customerId) 3423 | 3424 | if (*result)["amount"] != 1000 { 3425 | t.Errorf("Expected amount to be 1000, got %v", (*result)["amount"]) 3426 | } 3427 | if (*result)["currency"] != "INR" { 3428 | t.Errorf("Expected currency to be 'INR', got '%s'", (*result)["currency"]) 3429 | } 3430 | // Should not have customer_id when empty 3431 | if _, exists := (*result)["customer_id"]; exists { 3432 | t.Error("Expected no customer_id when empty string provided") 3433 | } 3434 | }) 3435 | 3436 | } 3437 | 3438 | // Test for processPaymentResult function 3439 | func TestProcessPaymentResult(t *testing.T) { 3440 | t.Run("processPaymentResult", func(t *testing.T) { 3441 | paymentResult := map[string]interface{}{ 3442 | "razorpay_payment_id": "pay_test123", 3443 | "status": "created", 3444 | "next": []interface{}{ 3445 | map[string]interface{}{ 3446 | "action": "redirect", 3447 | "url": "https://example.com", 3448 | }, 3449 | }, 3450 | } 3451 | 3452 | result, err := processPaymentResult(paymentResult) 3453 | 3454 | if err != nil { 3455 | t.Errorf("Expected no error, got %v", err) 3456 | } 3457 | 3458 | if result["razorpay_payment_id"] != "pay_test123" { 3459 | t.Errorf("Expected payment ID, got %v", result["razorpay_payment_id"]) 3460 | } 3461 | 3462 | if result["status"] != "payment_initiated" { 3463 | t.Errorf("Expected status to be 'payment_initiated', got '%s'", 3464 | result["status"]) 3465 | } 3466 | }) 3467 | 3468 | t.Run("processPaymentResult - with error", func(t *testing.T) { 3469 | // Test with payment result that might cause an error 3470 | paymentResult := map[string]interface{}{ 3471 | "error": map[string]interface{}{ 3472 | "code": "BAD_REQUEST_ERROR", 3473 | "description": "Invalid payment data", 3474 | }, 3475 | } 3476 | 3477 | result, err := processPaymentResult(paymentResult) 3478 | 3479 | // The function should handle this gracefully 3480 | if err != nil && result == nil { 3481 | t.Logf("Expected behavior - got error: %v", err) 3482 | } else if result != nil { 3483 | // If no error, result should be properly processed 3484 | if result["status"] != "payment_initiated" { 3485 | t.Errorf("Expected status to be 'payment_initiated', got '%s'", 3486 | result["status"]) 3487 | } 3488 | } 3489 | }) 3490 | 3491 | } 3492 | 3493 | // Test for extractOtpSubmitURL function 3494 | func TestExtractOtpSubmitURL(t *testing.T) { 3495 | t.Run("extractOtpSubmitURL", func(t *testing.T) { 3496 | // Test with valid response data containing OTP submit URL 3497 | responseData := map[string]interface{}{ 3498 | "next": []interface{}{ 3499 | map[string]interface{}{ 3500 | "action": "redirect", 3501 | "url": "https://example.com/redirect", 3502 | }, 3503 | map[string]interface{}{ 3504 | "action": "otp_submit", 3505 | "url": "https://example.com/otp/submit", 3506 | }, 3507 | }, 3508 | } 3509 | 3510 | result := extractOtpSubmitURL(responseData) 3511 | if result != "https://example.com/otp/submit" { 3512 | t.Errorf("Expected OTP submit URL, got '%s'", result) 3513 | } 3514 | }) 3515 | 3516 | t.Run("extractOtpSubmitURL - no OTP action", func(t *testing.T) { 3517 | responseData := map[string]interface{}{ 3518 | "next": []interface{}{ 3519 | map[string]interface{}{ 3520 | "action": "redirect", 3521 | "url": "https://example.com/redirect", 3522 | }, 3523 | }, 3524 | } 3525 | 3526 | result := extractOtpSubmitURL(responseData) 3527 | if result != "" { 3528 | t.Errorf("Expected empty string, got '%s'", result) 3529 | } 3530 | }) 3531 | 3532 | t.Run("extractOtpSubmitURL - invalid input", func(t *testing.T) { 3533 | // Test with invalid input type 3534 | result := extractOtpSubmitURL("invalid_input") 3535 | if result != "" { 3536 | t.Errorf("Expected empty string for invalid input, got '%s'", result) 3537 | } 3538 | }) 3539 | 3540 | t.Run("extractOtpSubmitURL - no next field", func(t *testing.T) { 3541 | responseData := map[string]interface{}{ 3542 | "other_field": "value", 3543 | } 3544 | 3545 | result := extractOtpSubmitURL(responseData) 3546 | if result != "" { 3547 | t.Errorf("Expected empty string when no next field, got '%s'", result) 3548 | } 3549 | }) 3550 | } 3551 | 3552 | // TestPayments100PercentCoverage_FetchPayment tests FetchPayment coverage 3553 | func TestPayments100PercentCoverage_FetchPayment(t *testing.T) { 3554 | // Test FetchPayment with SDK errors 3555 | t.Run("FetchPayment - SDK error", func(t *testing.T) { 3556 | testCase := RazorpayToolTestCase{ 3557 | Name: "SDK error", 3558 | Request: map[string]interface{}{ 3559 | "payment_id": "pay_test123", 3560 | }, 3561 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3562 | return mock.NewHTTPClient( 3563 | mock.Endpoint{ 3564 | Path: "/v1/payments/pay_test123", 3565 | Method: "GET", 3566 | Response: map[string]interface{}{ 3567 | "error": map[string]interface{}{ 3568 | "code": "BAD_REQUEST_ERROR", 3569 | "description": "Invalid payment ID", 3570 | }, 3571 | }, 3572 | }, 3573 | ) 3574 | }, 3575 | ExpectError: true, 3576 | ExpectedErrMsg: "fetching payment failed", 3577 | } 3578 | runToolTest(t, testCase, FetchPayment, "Payment") 3579 | }) 3580 | 3581 | } 3582 | 3583 | // TestPayments100PercentCoverage_FetchPaymentCardDetails tests 3584 | // FetchPaymentCardDetails coverage 3585 | func TestPayments100PercentCoverage_FetchPaymentCardDetails(t *testing.T) { 3586 | // Test FetchPaymentCardDetails with SDK errors 3587 | t.Run("FetchPaymentCardDetails - SDK error", func(t *testing.T) { 3588 | testCase := RazorpayToolTestCase{ 3589 | Name: "SDK error", 3590 | Request: map[string]interface{}{ 3591 | "payment_id": "pay_test123", 3592 | }, 3593 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3594 | return mock.NewHTTPClient( 3595 | mock.Endpoint{ 3596 | Path: "/v1/payments/pay_test123/card", 3597 | Method: "GET", 3598 | Response: map[string]interface{}{ 3599 | "error": map[string]interface{}{ 3600 | "code": "BAD_REQUEST_ERROR", 3601 | "description": "Card details not available", 3602 | }, 3603 | }, 3604 | }, 3605 | ) 3606 | }, 3607 | ExpectError: true, 3608 | ExpectedErrMsg: "fetching card details failed", 3609 | } 3610 | runToolTest(t, testCase, FetchPaymentCardDetails, "PaymentCardDetails") 3611 | }) 3612 | 3613 | } 3614 | 3615 | // TestPayments100PercentCoverage_UpdatePayment tests UpdatePayment coverage 3616 | func TestPayments100PercentCoverage_UpdatePayment(t *testing.T) { 3617 | // Test UpdatePayment with SDK errors 3618 | t.Run("UpdatePayment - SDK error", func(t *testing.T) { 3619 | testCase := RazorpayToolTestCase{ 3620 | Name: "SDK error", 3621 | Request: map[string]interface{}{ 3622 | "payment_id": "pay_test123", 3623 | "notes": map[string]interface{}{ 3624 | "key": "value", 3625 | }, 3626 | }, 3627 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3628 | return mock.NewHTTPClient( 3629 | mock.Endpoint{ 3630 | Path: "/v1/payments/pay_test123", 3631 | Method: "PATCH", 3632 | Response: map[string]interface{}{ 3633 | "error": map[string]interface{}{ 3634 | "code": "BAD_REQUEST_ERROR", 3635 | "description": "Invalid notes", 3636 | }, 3637 | }, 3638 | }, 3639 | ) 3640 | }, 3641 | ExpectError: true, 3642 | ExpectedErrMsg: "updating payment failed", 3643 | } 3644 | runToolTest(t, testCase, UpdatePayment, "Payment") 3645 | }) 3646 | 3647 | } 3648 | 3649 | // TestPayments100PercentCoverage_CapturePayment tests CapturePayment coverage 3650 | func TestPayments100PercentCoverage_CapturePayment(t *testing.T) { 3651 | // Test CapturePayment with SDK errors 3652 | t.Run("CapturePayment - SDK error", func(t *testing.T) { 3653 | testCase := RazorpayToolTestCase{ 3654 | Name: "SDK error", 3655 | Request: map[string]interface{}{ 3656 | "payment_id": "pay_test123", 3657 | "amount": 1000, 3658 | "currency": "INR", 3659 | }, 3660 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3661 | return mock.NewHTTPClient( 3662 | mock.Endpoint{ 3663 | Path: "/v1/payments/pay_test123/capture", 3664 | Method: "POST", 3665 | Response: map[string]interface{}{ 3666 | "error": map[string]interface{}{ 3667 | "code": "BAD_REQUEST_ERROR", 3668 | "description": "Payment cannot be captured", 3669 | }, 3670 | }, 3671 | }, 3672 | ) 3673 | }, 3674 | ExpectError: true, 3675 | ExpectedErrMsg: "capturing payment failed", 3676 | } 3677 | runToolTest(t, testCase, CapturePayment, "Payment") 3678 | }) 3679 | 3680 | } 3681 | 3682 | // TestPayments100PercentCoverage_FetchAllPayments tests 3683 | // FetchAllPayments coverage 3684 | func TestPayments100PercentCoverage_FetchAllPayments(t *testing.T) { 3685 | // Test FetchAllPayments with SDK errors 3686 | t.Run("FetchAllPayments - SDK error", func(t *testing.T) { 3687 | testCase := RazorpayToolTestCase{ 3688 | Name: "SDK error", 3689 | Request: map[string]interface{}{ 3690 | "count": 10, 3691 | }, 3692 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3693 | return mock.NewHTTPClient( 3694 | mock.Endpoint{ 3695 | Path: "/v1/payments", 3696 | Method: "GET", 3697 | Response: map[string]interface{}{ 3698 | "error": map[string]interface{}{ 3699 | "code": "BAD_REQUEST_ERROR", 3700 | "description": "Invalid request", 3701 | }, 3702 | }, 3703 | }, 3704 | ) 3705 | }, 3706 | ExpectError: true, 3707 | ExpectedErrMsg: "fetching payments failed", 3708 | } 3709 | runToolTest(t, testCase, FetchAllPayments, "Collection") 3710 | }) 3711 | 3712 | } 3713 | 3714 | // TestPayments100PercentCoverage_ResendOtp tests ResendOtp coverage 3715 | func TestPayments100PercentCoverage_ResendOtp(t *testing.T) { 3716 | // Test ResendOtp with SDK errors 3717 | t.Run("ResendOtp - SDK error", func(t *testing.T) { 3718 | testCase := RazorpayToolTestCase{ 3719 | Name: "SDK error", 3720 | Request: map[string]interface{}{ 3721 | "payment_id": "pay_test123", 3722 | }, 3723 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3724 | return mock.NewHTTPClient( 3725 | mock.Endpoint{ 3726 | Path: "/v1/payments/pay_test123/otp/resend", 3727 | Method: "POST", 3728 | Response: map[string]interface{}{ 3729 | "error": map[string]interface{}{ 3730 | "code": "BAD_REQUEST_ERROR", 3731 | "description": "Cannot resend OTP", 3732 | }, 3733 | }, 3734 | }, 3735 | ) 3736 | }, 3737 | ExpectError: true, 3738 | ExpectedErrMsg: "OTP resend failed", 3739 | } 3740 | runToolTest(t, testCase, ResendOtp, "ResendOtp") 3741 | }) 3742 | 3743 | } 3744 | 3745 | // TestPayments100PercentCoverage_SubmitOtp tests SubmitOtp coverage 3746 | func TestPayments100PercentCoverage_SubmitOtp(t *testing.T) { 3747 | // Test SubmitOtp with SDK errors 3748 | t.Run("SubmitOtp - SDK error", func(t *testing.T) { 3749 | testCase := RazorpayToolTestCase{ 3750 | Name: "SDK error", 3751 | Request: map[string]interface{}{ 3752 | "payment_id": "pay_test123", 3753 | "otp_string": "123456", 3754 | }, 3755 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3756 | return mock.NewHTTPClient( 3757 | mock.Endpoint{ 3758 | Path: "/v1/payments/pay_test123/otp/submit", 3759 | Method: "POST", 3760 | Response: map[string]interface{}{ 3761 | "error": map[string]interface{}{ 3762 | "code": "BAD_REQUEST_ERROR", 3763 | "description": "Invalid OTP", 3764 | }, 3765 | }, 3766 | }, 3767 | ) 3768 | }, 3769 | ExpectError: true, 3770 | ExpectedErrMsg: "OTP verification failed", 3771 | } 3772 | runToolTest(t, testCase, SubmitOtp, "SubmitOtp") 3773 | }) 3774 | 3775 | } 3776 | 3777 | // TestPayments100PercentCoverage_InitiatePayment tests InitiatePayment coverage 3778 | func TestPayments100PercentCoverage_InitiatePayment(t *testing.T) { 3779 | 3780 | // Test InitiatePayment with errors 3781 | t.Run("InitiatePayment - SDK error", func(t *testing.T) { 3782 | testCase := RazorpayToolTestCase{ 3783 | Name: "SDK error", 3784 | Request: map[string]interface{}{ 3785 | "amount": 1000, 3786 | "currency": "INR", 3787 | "order_id": "order_test123", 3788 | }, 3789 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3790 | return mock.NewHTTPClient( 3791 | mock.Endpoint{ 3792 | Path: "/v1/payments/create/json", 3793 | Method: "POST", 3794 | Response: map[string]interface{}{ 3795 | "error": map[string]interface{}{ 3796 | "code": "BAD_REQUEST_ERROR", 3797 | "description": "Invalid payment data", 3798 | }, 3799 | }, 3800 | }, 3801 | ) 3802 | }, 3803 | ExpectError: true, 3804 | ExpectedErrMsg: "initiating payment failed", 3805 | } 3806 | runToolTest(t, testCase, InitiatePayment, "InitiatePayment") 3807 | }) 3808 | 3809 | // Test sendOtp with HTTP error status 3810 | t.Run("sendOtp - HTTP error status", func(t *testing.T) { 3811 | server := httptest.NewTLSServer( 3812 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3813 | w.WriteHeader(http.StatusBadRequest) 3814 | })) 3815 | defer server.Close() 3816 | 3817 | // Replace domain to pass validation 3818 | testURL := strings.Replace( 3819 | server.URL, server.URL[8:], "api.razorpay.com/v1/payments/otp", 1) 3820 | err := sendOtp(testURL) 3821 | if err == nil { 3822 | t.Error("Expected error for HTTP error status") 3823 | } 3824 | if !strings.Contains(err.Error(), "OTP generation failed with HTTP status") { 3825 | t.Logf("Got expected error: %s", err.Error()) 3826 | } 3827 | }) 3828 | 3829 | // More aggressive tests - hitting every error path! 3830 | t.Run("sendOtp - request creation error", func(t *testing.T) { 3831 | // Test with malformed URL that passes parsing but fails request creation 3832 | err := sendOtp("https://api.razorpay.com:99999/invalid") 3833 | if err == nil { 3834 | t.Error("Expected error for malformed URL") 3835 | } 3836 | if !strings.Contains(err.Error(), "failed to create OTP request") && 3837 | !strings.Contains(err.Error(), "OTP generation failed") { 3838 | t.Logf("Got expected error: %s", err.Error()) 3839 | } 3840 | }) 3841 | 3842 | // Test with extremely long URL to trigger different error paths 3843 | t.Run("sendOtp - extreme URL", func(t *testing.T) { 3844 | longPath := strings.Repeat("a", 10000) 3845 | testURL := "https://api.razorpay.com/v1/payments/" + longPath + "/otp" 3846 | err := sendOtp(testURL) 3847 | if err == nil { 3848 | t.Error("Expected error for extreme URL") 3849 | } 3850 | }) 3851 | 3852 | } 3853 | 3854 | // contextKey is a type for context keys to avoid collisions 3855 | type contextKey string 3856 | 3857 | // TestPayments100PercentCoverage_ContextErrors tests context error paths 3858 | func TestPayments100PercentCoverage_ContextErrors(t *testing.T) { 3859 | // Test getClientFromContextOrDefault error path 3860 | t.Run("SubmitOtp - client context error", func(t *testing.T) { 3861 | // Create context with invalid client 3862 | ctx := context.WithValue( 3863 | context.Background(), contextKey("invalid_key"), "invalid_value") 3864 | 3865 | tool := SubmitOtp(nil, nil) 3866 | request := mcpgo.CallToolRequest{ 3867 | Arguments: map[string]interface{}{ 3868 | "payment_id": "pay_test123", 3869 | "otp_string": "123456", 3870 | }, 3871 | } 3872 | 3873 | result, err := tool.GetHandler()(ctx, request) 3874 | if err != nil { 3875 | t.Errorf("Expected no error, got %v", err) 3876 | } 3877 | if result == nil || !result.IsError { 3878 | t.Error("Expected error result for invalid client context") 3879 | } 3880 | }) 3881 | 3882 | // Test sendOtp with actual HTTP client failure 3883 | t.Run("sendOtp - HTTP client failure", func(t *testing.T) { 3884 | // Test with a URL that will fail at the HTTP client level 3885 | err := sendOtp("https://api.razorpay.com:99999/invalid/path/that/will/fail") 3886 | if err == nil { 3887 | t.Error("Expected error for HTTP client failure") 3888 | } 3889 | if !strings.Contains(err.Error(), "OTP generation failed") { 3890 | t.Logf("Got expected error: %s", err.Error()) 3891 | } 3892 | }) 3893 | 3894 | } 3895 | 3896 | // TestPayments100PercentCoverage_ContextErrors2 tests more context error paths 3897 | func TestPayments100PercentCoverage_ContextErrors2(t *testing.T) { 3898 | // Test InitiatePayment - getClientFromContextOrDefault error 3899 | t.Run("InitiatePayment - client context error", func(t *testing.T) { 3900 | // Create context without client 3901 | ctx := context.Background() 3902 | 3903 | tool := InitiatePayment(nil, nil) 3904 | request := mcpgo.CallToolRequest{ 3905 | Arguments: map[string]interface{}{ 3906 | "amount": 1000, 3907 | "order_id": "order_test123", 3908 | }, 3909 | } 3910 | 3911 | result, err := tool.GetHandler()(ctx, request) 3912 | if err != nil { 3913 | t.Errorf("Expected no error, got %v", err) 3914 | } 3915 | if result == nil || !result.IsError { 3916 | t.Error("Expected error result for missing client context") 3917 | } 3918 | if !strings.Contains(result.Text, "no client found in context") { 3919 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 3920 | } 3921 | }) 3922 | 3923 | // Simple working test 3924 | t.Run("InitiatePayment - basic test", func(t *testing.T) { 3925 | testCase := RazorpayToolTestCase{ 3926 | Name: "basic test", 3927 | Request: map[string]interface{}{ 3928 | "amount": 1000, 3929 | "order_id": "order_test123", 3930 | }, 3931 | MockHttpClient: func() (*http.Client, *httptest.Server) { 3932 | return mock.NewHTTPClient( 3933 | mock.Endpoint{ 3934 | Path: "/v1/payments/create/json", 3935 | Method: "POST", 3936 | Response: map[string]interface{}{ 3937 | "id": "pay_test123", 3938 | "entity": "payment", 3939 | "amount": 1000, 3940 | "currency": "INR", 3941 | "status": "created", 3942 | "invalid": make(chan int), // This causes JSON marshal to fail 3943 | "created_at": 1234567890, 3944 | }, 3945 | }, 3946 | ) 3947 | }, 3948 | ExpectError: true, 3949 | ExpectedErrMsg: "failed", 3950 | } 3951 | runToolTest(t, testCase, InitiatePayment, "Payment") 3952 | }) 3953 | 3954 | } 3955 | 3956 | // TestPayments100PercentCoverage_ContextErrors3 tests remaining 3957 | // context error paths 3958 | func TestPayments100PercentCoverage_ContextErrors3(t *testing.T) { 3959 | // Test FetchPayment - getClientFromContextOrDefault error 3960 | t.Run("FetchPayment - client context error", func(t *testing.T) { 3961 | // Create context without client 3962 | ctx := context.Background() 3963 | 3964 | tool := FetchPayment(nil, nil) 3965 | request := mcpgo.CallToolRequest{ 3966 | Arguments: map[string]interface{}{ 3967 | "payment_id": "pay_test123", 3968 | }, 3969 | } 3970 | 3971 | result, err := tool.GetHandler()(ctx, request) 3972 | if err != nil { 3973 | t.Errorf("Expected no error, got %v", err) 3974 | } 3975 | if result == nil || !result.IsError { 3976 | t.Error("Expected error result for missing client context") 3977 | } 3978 | if !strings.Contains(result.Text, "no client found in context") { 3979 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 3980 | } 3981 | }) 3982 | 3983 | // Test FetchPaymentCardDetails - getClientFromContextOrDefault error 3984 | t.Run("FetchPaymentCardDetails - client context error", func(t *testing.T) { 3985 | // Create context without client 3986 | ctx := context.Background() 3987 | 3988 | tool := FetchPaymentCardDetails(nil, nil) 3989 | request := mcpgo.CallToolRequest{ 3990 | Arguments: map[string]interface{}{ 3991 | "payment_id": "pay_test123", 3992 | }, 3993 | } 3994 | 3995 | result, err := tool.GetHandler()(ctx, request) 3996 | if err != nil { 3997 | t.Errorf("Expected no error, got %v", err) 3998 | } 3999 | if result == nil || !result.IsError { 4000 | t.Error("Expected error result for missing client context") 4001 | } 4002 | if !strings.Contains(result.Text, "no client found in context") { 4003 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 4004 | } 4005 | }) 4006 | 4007 | // Test FetchPaymentCardDetails - JSON marshal error 4008 | // (using channel in response) 4009 | t.Run("FetchPaymentCardDetails - JSON marshal error", func(t *testing.T) { 4010 | testCase := RazorpayToolTestCase{ 4011 | Name: "JSON marshal error", 4012 | Request: map[string]interface{}{ 4013 | "payment_id": "pay_test123", 4014 | }, 4015 | MockHttpClient: func() (*http.Client, *httptest.Server) { 4016 | return mock.NewHTTPClient( 4017 | mock.Endpoint{ 4018 | Path: "/v1/payments/pay_test123/card", 4019 | Method: "GET", 4020 | Response: map[string]interface{}{ 4021 | "id": "card_test123", 4022 | "entity": "card", 4023 | "name": "Test User", 4024 | "last4": "1234", 4025 | "network": "Visa", 4026 | "type": "credit", 4027 | "invalid": make(chan int), // This causes JSON marshal to fail 4028 | "created_at": 1234567890, 4029 | }, 4030 | }, 4031 | ) 4032 | }, 4033 | ExpectError: true, 4034 | ExpectedErrMsg: "failed", 4035 | } 4036 | runToolTest(t, testCase, FetchPaymentCardDetails, "Card Details") 4037 | }) 4038 | 4039 | } 4040 | 4041 | // TestPayments100PercentCoverage_ContextErrors4 tests final context error paths 4042 | func TestPayments100PercentCoverage_ContextErrors4(t *testing.T) { 4043 | // Test CapturePayment - getClientFromContextOrDefault error 4044 | t.Run("CapturePayment - client context error", func(t *testing.T) { 4045 | // Create context without client 4046 | ctx := context.Background() 4047 | 4048 | tool := CapturePayment(nil, nil) 4049 | request := mcpgo.CallToolRequest{ 4050 | Arguments: map[string]interface{}{ 4051 | "payment_id": "pay_test123", 4052 | "amount": 1000, 4053 | "currency": "INR", 4054 | }, 4055 | } 4056 | 4057 | result, err := tool.GetHandler()(ctx, request) 4058 | if err != nil { 4059 | t.Errorf("Expected no error, got %v", err) 4060 | } 4061 | if result == nil || !result.IsError { 4062 | t.Error("Expected error result for missing client context") 4063 | } 4064 | if !strings.Contains(result.Text, "no client found in context") { 4065 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 4066 | } 4067 | }) 4068 | 4069 | // Test CapturePayment - JSON marshal error (using channel in response) 4070 | t.Run("CapturePayment - JSON marshal error", func(t *testing.T) { 4071 | testCase := RazorpayToolTestCase{ 4072 | Name: "JSON marshal error", 4073 | Request: map[string]interface{}{ 4074 | "payment_id": "pay_test123", 4075 | "amount": 1000, 4076 | "currency": "INR", 4077 | }, 4078 | MockHttpClient: func() (*http.Client, *httptest.Server) { 4079 | return mock.NewHTTPClient( 4080 | mock.Endpoint{ 4081 | Path: "/v1/payments/pay_test123/capture", 4082 | Method: "POST", 4083 | Response: map[string]interface{}{ 4084 | "id": "pay_test123", 4085 | "entity": "payment", 4086 | "amount": 1000, 4087 | "currency": "INR", 4088 | "status": "captured", 4089 | "invalid": make(chan int), // This causes JSON marshal to fail 4090 | "created_at": 1234567890, 4091 | }, 4092 | }, 4093 | ) 4094 | }, 4095 | ExpectError: true, 4096 | ExpectedErrMsg: "failed", 4097 | } 4098 | runToolTest(t, testCase, CapturePayment, "Payment") 4099 | }) 4100 | 4101 | // Test UpdatePayment - getClientFromContextOrDefault error 4102 | t.Run("UpdatePayment - client context error", func(t *testing.T) { 4103 | // Create context without client 4104 | ctx := context.Background() 4105 | 4106 | tool := UpdatePayment(nil, nil) 4107 | request := mcpgo.CallToolRequest{ 4108 | Arguments: map[string]interface{}{ 4109 | "payment_id": "pay_test123", 4110 | "notes": map[string]interface{}{ 4111 | "key": "value", 4112 | }, 4113 | }, 4114 | } 4115 | 4116 | result, err := tool.GetHandler()(ctx, request) 4117 | if err != nil { 4118 | t.Errorf("Expected no error, got %v", err) 4119 | } 4120 | if result == nil || !result.IsError { 4121 | t.Error("Expected error result for missing client context") 4122 | } 4123 | if !strings.Contains(result.Text, "no client found in context") { 4124 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 4125 | } 4126 | }) 4127 | 4128 | // Test UpdatePayment - JSON marshal error (using channel in response) 4129 | t.Run("UpdatePayment - JSON marshal error", func(t *testing.T) { 4130 | testCase := RazorpayToolTestCase{ 4131 | Name: "JSON marshal error", 4132 | Request: map[string]interface{}{ 4133 | "payment_id": "pay_test123", 4134 | "notes": map[string]interface{}{ 4135 | "key": "value", 4136 | }, 4137 | }, 4138 | MockHttpClient: func() (*http.Client, *httptest.Server) { 4139 | return mock.NewHTTPClient( 4140 | mock.Endpoint{ 4141 | Path: "/v1/payments/pay_test123", 4142 | Method: "PATCH", 4143 | Response: map[string]interface{}{ 4144 | "id": "pay_test123", 4145 | "entity": "payment", 4146 | "amount": 1000, 4147 | "currency": "INR", 4148 | "status": "authorized", 4149 | "invalid": make(chan int), // This causes JSON marshal to fail 4150 | "notes": map[string]interface{}{ 4151 | "key": "value", 4152 | }, 4153 | "created_at": 1234567890, 4154 | }, 4155 | }, 4156 | ) 4157 | }, 4158 | ExpectError: true, 4159 | ExpectedErrMsg: "failed", 4160 | } 4161 | runToolTest(t, testCase, UpdatePayment, "Payment") 4162 | }) 4163 | 4164 | // Test FetchAllPayments - getClientFromContextOrDefault error 4165 | t.Run("FetchAllPayments - client context error", func(t *testing.T) { 4166 | // Create context without client 4167 | ctx := context.Background() 4168 | 4169 | tool := FetchAllPayments(nil, nil) 4170 | request := mcpgo.CallToolRequest{ 4171 | Arguments: map[string]interface{}{ 4172 | "count": 10, 4173 | }, 4174 | } 4175 | 4176 | result, err := tool.GetHandler()(ctx, request) 4177 | if err != nil { 4178 | t.Errorf("Expected no error, got %v", err) 4179 | } 4180 | if result == nil || !result.IsError { 4181 | t.Error("Expected error result for missing client context") 4182 | } 4183 | if !strings.Contains(result.Text, "no client found in context") { 4184 | t.Errorf("Expected 'no client found in context', got '%s'", result.Text) 4185 | } 4186 | }) 4187 | 4188 | // Test FetchAllPayments - JSON marshal error (using channel in response) 4189 | t.Run("FetchAllPayments - JSON marshal error", func(t *testing.T) { 4190 | testCase := RazorpayToolTestCase{ 4191 | Name: "JSON marshal error", 4192 | Request: map[string]interface{}{ 4193 | "count": 10, 4194 | }, 4195 | MockHttpClient: func() (*http.Client, *httptest.Server) { 4196 | return mock.NewHTTPClient( 4197 | mock.Endpoint{ 4198 | Path: "/v1/payments", 4199 | Method: "GET", 4200 | Response: map[string]interface{}{ 4201 | "entity": "collection", 4202 | "count": 1, 4203 | "items": []interface{}{ 4204 | map[string]interface{}{ 4205 | "id": "pay_test123", 4206 | "invalid": make(chan int), // This causes JSON marshal to fail 4207 | "entity": "payment", 4208 | "amount": 1000, 4209 | "currency": "INR", 4210 | "status": "created", 4211 | "created_at": 1234567890, 4212 | }, 4213 | }, 4214 | }, 4215 | }, 4216 | ) 4217 | }, 4218 | ExpectError: true, 4219 | ExpectedErrMsg: "failed", 4220 | } 4221 | runToolTest(t, testCase, FetchAllPayments, "Collection") 4222 | }) 4223 | } 4224 | ```