This is page 3 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
│ ├── CODEOWNERS
│ ├── 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_test.go
│ ├── main.go
│ ├── stdio_test.go
│ └── stdio.go
├── codecov.yml
├── CONTRIBUTING.md
├── coverage.out
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── pkg
│ ├── contextkey
│ │ ├── context_key_test.go
│ │ └── context_key.go
│ ├── log
│ │ ├── config_test.go
│ │ ├── config.go
│ │ ├── log.go
│ │ ├── slog_test.go
│ │ └── slog.go
│ ├── mcpgo
│ │ ├── README.md
│ │ ├── server_test.go
│ │ ├── server.go
│ │ ├── stdio_test.go
│ │ ├── stdio.go
│ │ ├── tool_test.go
│ │ ├── tool.go
│ │ └── transport.go
│ ├── observability
│ │ ├── observability_test.go
│ │ └── 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_test.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_test.go
│ └── toolsets.go
├── README.md
└── SECURITY.md
```
# Files
--------------------------------------------------------------------------------
/pkg/razorpay/payment_links_test.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/razorpay/razorpay-go/constants"
10 |
11 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
12 | )
13 |
14 | func Test_CreatePaymentLink(t *testing.T) {
15 | createPaymentLinkPath := fmt.Sprintf(
16 | "/%s%s",
17 | constants.VERSION_V1,
18 | constants.PaymentLink_URL,
19 | )
20 |
21 | successfulPaymentLinkResp := map[string]interface{}{
22 | "id": "plink_ExjpAUN3gVHrPJ",
23 | "amount": float64(50000),
24 | "currency": "INR",
25 | "description": "Test payment",
26 | "status": "created",
27 | "short_url": "https://rzp.io/i/nxrHnLJ",
28 | }
29 |
30 | paymentLinkWithoutDescResp := map[string]interface{}{
31 | "id": "plink_ExjpAUN3gVHrPJ",
32 | "amount": float64(50000),
33 | "currency": "INR",
34 | "status": "created",
35 | "short_url": "https://rzp.io/i/nxrHnLJ",
36 | }
37 |
38 | invalidCurrencyErrorResp := map[string]interface{}{
39 | "error": map[string]interface{}{
40 | "code": "BAD_REQUEST_ERROR",
41 | "description": "API error: Invalid currency",
42 | },
43 | }
44 |
45 | tests := []RazorpayToolTestCase{
46 | {
47 | Name: "successful payment link creation",
48 | Request: map[string]interface{}{
49 | "amount": float64(50000),
50 | "currency": "INR",
51 | "description": "Test payment",
52 | },
53 | MockHttpClient: func() (*http.Client, *httptest.Server) {
54 | return mock.NewHTTPClient(
55 | mock.Endpoint{
56 | Path: createPaymentLinkPath,
57 | Method: "POST",
58 | Response: successfulPaymentLinkResp,
59 | },
60 | )
61 | },
62 | ExpectError: false,
63 | ExpectedResult: successfulPaymentLinkResp,
64 | },
65 | {
66 | Name: "payment link without description",
67 | Request: map[string]interface{}{
68 | "amount": float64(50000),
69 | "currency": "INR",
70 | },
71 | MockHttpClient: func() (*http.Client, *httptest.Server) {
72 | return mock.NewHTTPClient(
73 | mock.Endpoint{
74 | Path: createPaymentLinkPath,
75 | Method: "POST",
76 | Response: paymentLinkWithoutDescResp,
77 | },
78 | )
79 | },
80 | ExpectError: false,
81 | ExpectedResult: paymentLinkWithoutDescResp,
82 | },
83 | {
84 | Name: "missing amount parameter",
85 | Request: map[string]interface{}{
86 | "currency": "INR",
87 | },
88 | MockHttpClient: nil, // No HTTP client needed for validation error
89 | ExpectError: true,
90 | ExpectedErrMsg: "missing required parameter: amount",
91 | },
92 | {
93 | Name: "missing currency parameter",
94 | Request: map[string]interface{}{
95 | "amount": float64(50000),
96 | },
97 | MockHttpClient: nil, // No HTTP client needed for validation error
98 | ExpectError: true,
99 | ExpectedErrMsg: "missing required parameter: currency",
100 | },
101 | {
102 | Name: "multiple validation errors",
103 | Request: map[string]interface{}{
104 | // Missing both amount and currency (required parameters)
105 | "description": 12345, // Wrong type for description
106 | },
107 | MockHttpClient: nil, // No HTTP client needed for validation error
108 | ExpectError: true,
109 | ExpectedErrMsg: "Validation errors:\n- " +
110 | "missing required parameter: amount\n- " +
111 | "missing required parameter: currency\n- " +
112 | "invalid parameter type: description",
113 | },
114 | {
115 | Name: "payment link creation fails",
116 | Request: map[string]interface{}{
117 | "amount": float64(50000),
118 | "currency": "XYZ", // Invalid currency
119 | },
120 | MockHttpClient: func() (*http.Client, *httptest.Server) {
121 | return mock.NewHTTPClient(
122 | mock.Endpoint{
123 | Path: createPaymentLinkPath,
124 | Method: "POST",
125 | Response: invalidCurrencyErrorResp,
126 | },
127 | )
128 | },
129 | ExpectError: true,
130 | ExpectedErrMsg: "creating payment link failed: API error: Invalid currency",
131 | },
132 | }
133 |
134 | for _, tc := range tests {
135 | t.Run(tc.Name, func(t *testing.T) {
136 | runToolTest(t, tc, CreatePaymentLink, "Payment Link")
137 | })
138 | }
139 | }
140 |
141 | func Test_FetchPaymentLink(t *testing.T) {
142 | fetchPaymentLinkPathFmt := fmt.Sprintf(
143 | "/%s%s/%%s",
144 | constants.VERSION_V1,
145 | constants.PaymentLink_URL,
146 | )
147 |
148 | // Define common response maps to be reused
149 | paymentLinkResp := map[string]interface{}{
150 | "id": "plink_ExjpAUN3gVHrPJ",
151 | "amount": float64(50000),
152 | "currency": "INR",
153 | "description": "Test payment",
154 | "status": "paid",
155 | "short_url": "https://rzp.io/i/nxrHnLJ",
156 | }
157 |
158 | paymentLinkNotFoundResp := map[string]interface{}{
159 | "error": map[string]interface{}{
160 | "code": "BAD_REQUEST_ERROR",
161 | "description": "payment link not found",
162 | },
163 | }
164 |
165 | tests := []RazorpayToolTestCase{
166 | {
167 | Name: "successful payment link fetch",
168 | Request: map[string]interface{}{
169 | "payment_link_id": "plink_ExjpAUN3gVHrPJ",
170 | },
171 | MockHttpClient: func() (*http.Client, *httptest.Server) {
172 | return mock.NewHTTPClient(
173 | mock.Endpoint{
174 | Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_ExjpAUN3gVHrPJ"),
175 | Method: "GET",
176 | Response: paymentLinkResp,
177 | },
178 | )
179 | },
180 | ExpectError: false,
181 | ExpectedResult: paymentLinkResp,
182 | },
183 | {
184 | Name: "payment link not found",
185 | Request: map[string]interface{}{
186 | "payment_link_id": "plink_invalid",
187 | },
188 | MockHttpClient: func() (*http.Client, *httptest.Server) {
189 | return mock.NewHTTPClient(
190 | mock.Endpoint{
191 | Path: fmt.Sprintf(fetchPaymentLinkPathFmt, "plink_invalid"),
192 | Method: "GET",
193 | Response: paymentLinkNotFoundResp,
194 | },
195 | )
196 | },
197 | ExpectError: true,
198 | ExpectedErrMsg: "fetching payment link failed: payment link not found",
199 | },
200 | {
201 | Name: "missing payment_link_id parameter",
202 | Request: map[string]interface{}{},
203 | MockHttpClient: nil, // No HTTP client needed for validation error
204 | ExpectError: true,
205 | ExpectedErrMsg: "missing required parameter: payment_link_id",
206 | },
207 | {
208 | Name: "multiple validation errors",
209 | Request: map[string]interface{}{
210 | // Missing payment_link_id parameter
211 | "non_existent_param": 12345, // Additional parameter that doesn't exist
212 | },
213 | MockHttpClient: nil, // No HTTP client needed for validation error
214 | ExpectError: true,
215 | ExpectedErrMsg: "missing required parameter: payment_link_id",
216 | },
217 | }
218 |
219 | for _, tc := range tests {
220 | t.Run(tc.Name, func(t *testing.T) {
221 | runToolTest(t, tc, FetchPaymentLink, "Payment Link")
222 | })
223 | }
224 | }
225 |
226 | func Test_CreateUpiPaymentLink(t *testing.T) {
227 | createPaymentLinkPath := fmt.Sprintf(
228 | "/%s%s",
229 | constants.VERSION_V1,
230 | constants.PaymentLink_URL,
231 | )
232 |
233 | upiPaymentLinkWithAllParamsResp := map[string]interface{}{
234 | "id": "plink_UpiAllParamsExjpAUN3gVHrPJ",
235 | "amount": float64(50000),
236 | "currency": "INR",
237 | "description": "Test UPI payment with all params",
238 | "reference_id": "REF12345",
239 | "accept_partial": true,
240 | "expire_by": float64(1718196584),
241 | "reminder_enable": true,
242 | "status": "created",
243 | "short_url": "https://rzp.io/i/upiAllParams123",
244 | "upi_link": true,
245 | "customer": map[string]interface{}{
246 | "name": "Test Customer",
247 | "email": "[email protected]",
248 | "contact": "+919876543210",
249 | },
250 | "notes": map[string]interface{}{
251 | "policy_name": "Test Policy",
252 | "user_id": "usr_123",
253 | },
254 | }
255 |
256 | errorResp := map[string]interface{}{
257 | "error": map[string]interface{}{
258 | "code": "BAD_REQUEST_ERROR",
259 | "description": "API error: Something went wrong",
260 | },
261 | }
262 |
263 | tests := []RazorpayToolTestCase{
264 | {
265 | Name: "UPI payment link with all parameters",
266 | Request: map[string]interface{}{
267 | "amount": float64(50000),
268 | "currency": "INR",
269 | "description": "Test UPI payment with all params",
270 | "reference_id": "REF12345",
271 | "accept_partial": true,
272 | "first_min_partial_amount": float64(10000),
273 | "expire_by": float64(1718196584),
274 | "customer_name": "Test Customer",
275 | "customer_email": "[email protected]",
276 | "customer_contact": "+919876543210",
277 | "notify_sms": true,
278 | "notify_email": true,
279 | "reminder_enable": true,
280 | "notes": map[string]interface{}{
281 | "policy_name": "Test Policy",
282 | "user_id": "usr_123",
283 | },
284 | "callback_url": "https://example.com/callback",
285 | "callback_method": "get",
286 | },
287 | MockHttpClient: func() (*http.Client, *httptest.Server) {
288 | return mock.NewHTTPClient(
289 | mock.Endpoint{
290 | Path: createPaymentLinkPath,
291 | Method: "POST",
292 | Response: upiPaymentLinkWithAllParamsResp,
293 | },
294 | )
295 | },
296 | ExpectError: false,
297 | ExpectedResult: upiPaymentLinkWithAllParamsResp,
298 | },
299 | {
300 | Name: "missing amount parameter",
301 | Request: map[string]interface{}{},
302 | MockHttpClient: nil, // No HTTP client needed for validation error
303 | ExpectError: true,
304 | ExpectedErrMsg: "missing required parameter: amount",
305 | },
306 | {
307 | Name: "UPI payment link creation fails",
308 | Request: map[string]interface{}{
309 | "amount": float64(50000),
310 | },
311 | MockHttpClient: func() (*http.Client, *httptest.Server) {
312 | return mock.NewHTTPClient(
313 | mock.Endpoint{
314 | Path: createPaymentLinkPath,
315 | Method: "POST",
316 | Response: errorResp,
317 | },
318 | )
319 | },
320 | ExpectError: true,
321 | ExpectedErrMsg: "missing required parameter: currency",
322 | },
323 | }
324 |
325 | for _, tc := range tests {
326 | t.Run(tc.Name, func(t *testing.T) {
327 | runToolTest(t, tc, CreateUpiPaymentLink, "UPI Payment Link")
328 | })
329 | }
330 | }
331 |
332 | func Test_ResendPaymentLinkNotification(t *testing.T) {
333 | notifyPaymentLinkPathFmt := fmt.Sprintf(
334 | "/%s%s/%%s/notify_by/%%s",
335 | constants.VERSION_V1,
336 | constants.PaymentLink_URL,
337 | )
338 |
339 | successResponse := map[string]interface{}{
340 | "success": true,
341 | }
342 |
343 | invalidMediumErrorResp := map[string]interface{}{
344 | "error": map[string]interface{}{
345 | "code": "BAD_REQUEST_ERROR",
346 | "description": "not a valid notification medium",
347 | },
348 | }
349 |
350 | tests := []RazorpayToolTestCase{
351 | {
352 | Name: "successful SMS notification",
353 | Request: map[string]interface{}{
354 | "payment_link_id": "plink_ExjpAUN3gVHrPJ",
355 | "medium": "sms",
356 | },
357 | MockHttpClient: func() (*http.Client, *httptest.Server) {
358 | return mock.NewHTTPClient(
359 | mock.Endpoint{
360 | Path: fmt.Sprintf(
361 | notifyPaymentLinkPathFmt,
362 | "plink_ExjpAUN3gVHrPJ",
363 | "sms",
364 | ),
365 | Method: "POST",
366 | Response: successResponse,
367 | },
368 | )
369 | },
370 | ExpectError: false,
371 | ExpectedResult: successResponse,
372 | },
373 | {
374 | Name: "missing payment_link_id parameter",
375 | Request: map[string]interface{}{
376 | "medium": "sms",
377 | },
378 | MockHttpClient: nil, // No HTTP client needed for validation error
379 | ExpectError: true,
380 | ExpectedErrMsg: "missing required parameter: payment_link_id",
381 | },
382 | {
383 | Name: "missing medium parameter",
384 | Request: map[string]interface{}{
385 | "payment_link_id": "plink_ExjpAUN3gVHrPJ",
386 | },
387 | MockHttpClient: nil, // No HTTP client needed for validation error
388 | ExpectError: true,
389 | ExpectedErrMsg: "missing required parameter: medium",
390 | },
391 | {
392 | Name: "API error response",
393 | Request: map[string]interface{}{
394 | "payment_link_id": "plink_Invalid",
395 | "medium": "sms", // Using valid medium so it passes validation
396 | },
397 | MockHttpClient: func() (*http.Client, *httptest.Server) {
398 | return mock.NewHTTPClient(
399 | mock.Endpoint{
400 | Path: fmt.Sprintf(
401 | notifyPaymentLinkPathFmt,
402 | "plink_Invalid",
403 | "sms",
404 | ),
405 | Method: "POST",
406 | Response: invalidMediumErrorResp,
407 | },
408 | )
409 | },
410 | ExpectError: true,
411 | ExpectedErrMsg: "sending notification failed: " +
412 | "not a valid notification medium",
413 | },
414 | }
415 |
416 | for _, tc := range tests {
417 | t.Run(tc.Name, func(t *testing.T) {
418 | toolFunc := ResendPaymentLinkNotification
419 | runToolTest(t, tc, toolFunc, "Payment Link Notification")
420 | })
421 | }
422 | }
423 |
424 | func Test_UpdatePaymentLink(t *testing.T) {
425 | updatePaymentLinkPathFmt := fmt.Sprintf(
426 | "/%s%s/%%s",
427 | constants.VERSION_V1,
428 | constants.PaymentLink_URL,
429 | )
430 |
431 | updatedPaymentLinkResp := map[string]interface{}{
432 | "id": "plink_FL5HCrWEO112OW",
433 | "amount": float64(1000),
434 | "currency": "INR",
435 | "status": "created",
436 | "reference_id": "TS35",
437 | "expire_by": float64(1612092283),
438 | "reminder_enable": false,
439 | "notes": []interface{}{
440 | map[string]interface{}{
441 | "key": "policy_name",
442 | "value": "Jeevan Saral",
443 | },
444 | },
445 | }
446 |
447 | invalidStateResp := map[string]interface{}{
448 | "error": map[string]interface{}{
449 | "code": "BAD_REQUEST_ERROR",
450 | "description": "update can only be made in created or partially paid state",
451 | },
452 | }
453 |
454 | tests := []RazorpayToolTestCase{
455 | {
456 | Name: "successful update with multiple fields",
457 | Request: map[string]interface{}{
458 | "payment_link_id": "plink_FL5HCrWEO112OW",
459 | "reference_id": "TS35",
460 | "expire_by": float64(1612092283),
461 | "reminder_enable": false,
462 | "accept_partial": true,
463 | "notes": map[string]interface{}{
464 | "policy_name": "Jeevan Saral",
465 | },
466 | },
467 | MockHttpClient: func() (*http.Client, *httptest.Server) {
468 | return mock.NewHTTPClient(
469 | mock.Endpoint{
470 | Path: fmt.Sprintf(
471 | updatePaymentLinkPathFmt,
472 | "plink_FL5HCrWEO112OW",
473 | ),
474 | Method: "PATCH",
475 | Response: updatedPaymentLinkResp,
476 | },
477 | )
478 | },
479 | ExpectError: false,
480 | ExpectedResult: updatedPaymentLinkResp,
481 | },
482 | {
483 | Name: "successful update with single field",
484 | Request: map[string]interface{}{
485 | "payment_link_id": "plink_FL5HCrWEO112OW",
486 | "reference_id": "TS35",
487 | },
488 | MockHttpClient: func() (*http.Client, *httptest.Server) {
489 | return mock.NewHTTPClient(
490 | mock.Endpoint{
491 | Path: fmt.Sprintf(
492 | updatePaymentLinkPathFmt,
493 | "plink_FL5HCrWEO112OW",
494 | ),
495 | Method: "PATCH",
496 | Response: updatedPaymentLinkResp,
497 | },
498 | )
499 | },
500 | ExpectError: false,
501 | ExpectedResult: updatedPaymentLinkResp,
502 | },
503 | {
504 | Name: "missing payment_link_id parameter",
505 | Request: map[string]interface{}{
506 | "reference_id": "TS35",
507 | },
508 | MockHttpClient: nil, // No HTTP client needed for validation error
509 | ExpectError: true,
510 | ExpectedErrMsg: "missing required parameter: payment_link_id",
511 | },
512 | {
513 | Name: "no update fields provided",
514 | Request: map[string]interface{}{
515 | "payment_link_id": "plink_FL5HCrWEO112OW",
516 | },
517 | MockHttpClient: nil, // No HTTP client needed for validation error
518 | ExpectError: true,
519 | ExpectedErrMsg: "at least one field to update must be provided",
520 | },
521 | {
522 | Name: "payment link in invalid state",
523 | Request: map[string]interface{}{
524 | "payment_link_id": "plink_Paid",
525 | "reference_id": "TS35",
526 | },
527 | MockHttpClient: func() (*http.Client, *httptest.Server) {
528 | return mock.NewHTTPClient(
529 | mock.Endpoint{
530 | Path: fmt.Sprintf(
531 | updatePaymentLinkPathFmt,
532 | "plink_Paid",
533 | ),
534 | Method: "PATCH",
535 | Response: invalidStateResp,
536 | },
537 | )
538 | },
539 | ExpectError: true,
540 | ExpectedErrMsg: "updating payment link failed: update can only be made in " +
541 | "created or partially paid state",
542 | },
543 | {
544 | Name: "update with explicit false value",
545 | Request: map[string]interface{}{
546 | "payment_link_id": "plink_FL5HCrWEO112OW",
547 | "reminder_enable": false, // Explicitly set to false
548 | },
549 | MockHttpClient: func() (*http.Client, *httptest.Server) {
550 | return mock.NewHTTPClient(
551 | mock.Endpoint{
552 | Path: fmt.Sprintf(
553 | updatePaymentLinkPathFmt,
554 | "plink_FL5HCrWEO112OW",
555 | ),
556 | Method: "PATCH",
557 | Response: updatedPaymentLinkResp,
558 | },
559 | )
560 | },
561 | ExpectError: false,
562 | ExpectedResult: updatedPaymentLinkResp,
563 | },
564 | }
565 |
566 | for _, tc := range tests {
567 | t.Run(tc.Name, func(t *testing.T) {
568 | toolFunc := UpdatePaymentLink
569 | runToolTest(t, tc, toolFunc, "Payment Link Update")
570 | })
571 | }
572 | }
573 |
574 | func Test_FetchAllPaymentLinks(t *testing.T) {
575 | fetchAllPaymentLinksPath := fmt.Sprintf(
576 | "/%s%s",
577 | constants.VERSION_V1,
578 | constants.PaymentLink_URL,
579 | )
580 |
581 | allPaymentLinksResp := map[string]interface{}{
582 | "payment_links": []interface{}{
583 | map[string]interface{}{
584 | "id": "plink_KBnb7I424Rc1R9",
585 | "amount": float64(10000),
586 | "currency": "INR",
587 | "status": "paid",
588 | "description": "Grocery",
589 | "reference_id": "111",
590 | "short_url": "https://rzp.io/i/alaBxs0i",
591 | "upi_link": false,
592 | },
593 | map[string]interface{}{
594 | "id": "plink_JP6yOUDCuHgcrl",
595 | "amount": float64(10000),
596 | "currency": "INR",
597 | "status": "paid",
598 | "description": "Online Tutoring - 1 Month",
599 | "reference_id": "11212",
600 | "short_url": "https://rzp.io/i/0ioYuawFu",
601 | "upi_link": false,
602 | },
603 | },
604 | }
605 |
606 | errorResp := map[string]interface{}{
607 | "error": map[string]interface{}{
608 | "code": "BAD_REQUEST_ERROR",
609 | "description": "The api key/secret provided is invalid",
610 | },
611 | }
612 |
613 | tests := []RazorpayToolTestCase{
614 | {
615 | Name: "fetch all payment links",
616 | Request: map[string]interface{}{},
617 | MockHttpClient: func() (*http.Client, *httptest.Server) {
618 | return mock.NewHTTPClient(
619 | mock.Endpoint{
620 | Path: fetchAllPaymentLinksPath,
621 | Method: "GET",
622 | Response: allPaymentLinksResp,
623 | },
624 | )
625 | },
626 | ExpectError: false,
627 | ExpectedResult: allPaymentLinksResp,
628 | },
629 | {
630 | Name: "api error",
631 | Request: map[string]interface{}{},
632 | MockHttpClient: func() (*http.Client, *httptest.Server) {
633 | return mock.NewHTTPClient(
634 | mock.Endpoint{
635 | Path: fetchAllPaymentLinksPath,
636 | Method: "GET",
637 | Response: errorResp,
638 | },
639 | )
640 | },
641 | ExpectError: true,
642 | ExpectedErrMsg: "fetching payment links failed: The api key/secret provided is invalid", // nolint:lll
643 | },
644 | }
645 |
646 | for _, tc := range tests {
647 | t.Run(tc.Name, func(t *testing.T) {
648 | toolFunc := FetchAllPaymentLinks
649 | runToolTest(t, tc, toolFunc, "Payment Links")
650 | })
651 | }
652 | }
653 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/payment_links.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | rzpsdk "github.com/razorpay/razorpay-go"
8 |
9 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
10 | "github.com/razorpay/razorpay-mcp-server/pkg/observability"
11 | )
12 |
13 | // CreatePaymentLink returns a tool that creates payment links in Razorpay
14 | func CreatePaymentLink(
15 | obs *observability.Observability,
16 | client *rzpsdk.Client,
17 | ) mcpgo.Tool {
18 | parameters := []mcpgo.ToolParameter{
19 | mcpgo.WithNumber(
20 | "amount",
21 | mcpgo.Description("Amount to be paid using the link in smallest "+
22 | "currency unit(e.g., ₹300, use 30000)"),
23 | mcpgo.Required(),
24 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
25 | ),
26 | mcpgo.WithString(
27 | "currency",
28 | mcpgo.Description("Three-letter ISO code for the currency (e.g., INR)"),
29 | mcpgo.Required(),
30 | ),
31 | mcpgo.WithString(
32 | "description",
33 | mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
34 | ),
35 | mcpgo.WithBoolean(
36 | "accept_partial",
37 | mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
38 | ),
39 | mcpgo.WithNumber(
40 | "first_min_partial_amount",
41 | mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
42 | ),
43 | mcpgo.WithNumber(
44 | "expire_by",
45 | mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
46 | ),
47 | mcpgo.WithString(
48 | "reference_id",
49 | mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
50 | ),
51 | mcpgo.WithString(
52 | "customer_name",
53 | mcpgo.Description("Name of the customer."),
54 | ),
55 | mcpgo.WithString(
56 | "customer_email",
57 | mcpgo.Description("Email address of the customer."),
58 | ),
59 | mcpgo.WithString(
60 | "customer_contact",
61 | mcpgo.Description("Contact number of the customer."),
62 | ),
63 | mcpgo.WithBoolean(
64 | "notify_sms",
65 | mcpgo.Description("Send SMS notifications for the Payment Link."),
66 | ),
67 | mcpgo.WithBoolean(
68 | "notify_email",
69 | mcpgo.Description("Send email notifications for the Payment Link."),
70 | ),
71 | mcpgo.WithBoolean(
72 | "reminder_enable",
73 | mcpgo.Description("Enable payment reminders for the Payment Link."),
74 | ),
75 | mcpgo.WithObject(
76 | "notes",
77 | mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
78 | ),
79 | mcpgo.WithString(
80 | "callback_url",
81 | mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
82 | ),
83 | mcpgo.WithString(
84 | "callback_method",
85 | mcpgo.Description("HTTP method for callback redirection. "+
86 | "Must be 'get' if callback_url is set."),
87 | ),
88 | }
89 |
90 | handler := func(
91 | ctx context.Context,
92 | r mcpgo.CallToolRequest,
93 | ) (*mcpgo.ToolResult, error) {
94 | // Get client from context or use default
95 | client, err := getClientFromContextOrDefault(ctx, client)
96 | if err != nil {
97 | return mcpgo.NewToolResultError(err.Error()), nil
98 | }
99 |
100 | // Create a parameters map to collect validated parameters
101 | plCreateReq := make(map[string]interface{})
102 | customer := make(map[string]interface{})
103 | notify := make(map[string]interface{})
104 | // Validate all parameters with fluent validator
105 | validator := NewValidator(&r).
106 | ValidateAndAddRequiredInt(plCreateReq, "amount").
107 | ValidateAndAddRequiredString(plCreateReq, "currency").
108 | ValidateAndAddOptionalString(plCreateReq, "description").
109 | ValidateAndAddOptionalBool(plCreateReq, "accept_partial").
110 | ValidateAndAddOptionalInt(plCreateReq, "first_min_partial_amount").
111 | ValidateAndAddOptionalInt(plCreateReq, "expire_by").
112 | ValidateAndAddOptionalString(plCreateReq, "reference_id").
113 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
114 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
115 | ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
116 | ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
117 | ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
118 | ValidateAndAddOptionalBool(plCreateReq, "reminder_enable").
119 | ValidateAndAddOptionalMap(plCreateReq, "notes").
120 | ValidateAndAddOptionalString(plCreateReq, "callback_url").
121 | ValidateAndAddOptionalString(plCreateReq, "callback_method")
122 |
123 | if result, err := validator.HandleErrorsIfAny(); result != nil {
124 | return result, err
125 | }
126 |
127 | // Handle customer details
128 | if len(customer) > 0 {
129 | plCreateReq["customer"] = customer
130 | }
131 |
132 | // Handle notification settings
133 | if len(notify) > 0 {
134 | plCreateReq["notify"] = notify
135 | }
136 |
137 | // Create the payment link
138 | paymentLink, err := client.PaymentLink.Create(plCreateReq, nil)
139 | if err != nil {
140 | return mcpgo.NewToolResultError(
141 | fmt.Sprintf("creating payment link failed: %s", err.Error())), nil
142 | }
143 |
144 | return mcpgo.NewToolResultJSON(paymentLink)
145 | }
146 |
147 | return mcpgo.NewTool(
148 | "create_payment_link",
149 | "Create a new standard payment link in Razorpay with a specified amount",
150 | parameters,
151 | handler,
152 | )
153 | }
154 |
155 | // CreateUpiPaymentLink returns a tool that creates payment links in Razorpay
156 | func CreateUpiPaymentLink(
157 | obs *observability.Observability,
158 | client *rzpsdk.Client,
159 | ) mcpgo.Tool {
160 | parameters := []mcpgo.ToolParameter{
161 | mcpgo.WithNumber(
162 | "amount",
163 | mcpgo.Description("Amount to be paid using the link in smallest currency unit(e.g., ₹300, use 30000), Only accepted currency is INR"), // nolint:lll
164 | mcpgo.Required(),
165 | mcpgo.Min(100), // Minimum amount is 100 (1.00 in currency)
166 | ),
167 | mcpgo.WithString(
168 | "currency",
169 | mcpgo.Description("Three-letter ISO code for the currency (e.g., INR). UPI links are only supported in INR"), // nolint:lll
170 | mcpgo.Required(),
171 | ),
172 | mcpgo.WithString(
173 | "description",
174 | mcpgo.Description("A brief description of the Payment Link explaining the intent of the payment."), // nolint:lll
175 | ),
176 | mcpgo.WithBoolean(
177 | "accept_partial",
178 | mcpgo.Description("Indicates whether customers can make partial payments using the Payment Link. Default: false"), // nolint:lll
179 | ),
180 | mcpgo.WithNumber(
181 | "first_min_partial_amount",
182 | mcpgo.Description("Minimum amount that must be paid by the customer as the first partial payment. Default value is 100."), // nolint:lll
183 | ),
184 | mcpgo.WithNumber(
185 | "expire_by",
186 | mcpgo.Description("Timestamp, in Unix, when the Payment Link will expire. By default, a Payment Link will be valid for six months."), // nolint:lll
187 | ),
188 | mcpgo.WithString(
189 | "reference_id",
190 | mcpgo.Description("Reference number tagged to a Payment Link. Must be unique for each Payment Link. Max 40 characters."), // nolint:lll
191 | ),
192 | mcpgo.WithString(
193 | "customer_name",
194 | mcpgo.Description("Name of the customer."),
195 | ),
196 | mcpgo.WithString(
197 | "customer_email",
198 | mcpgo.Description("Email address of the customer."),
199 | ),
200 | mcpgo.WithString(
201 | "customer_contact",
202 | mcpgo.Description("Contact number of the customer."),
203 | ),
204 | mcpgo.WithBoolean(
205 | "notify_sms",
206 | mcpgo.Description("Send SMS notifications for the Payment Link."),
207 | ),
208 | mcpgo.WithBoolean(
209 | "notify_email",
210 | mcpgo.Description("Send email notifications for the Payment Link."),
211 | ),
212 | mcpgo.WithBoolean(
213 | "reminder_enable",
214 | mcpgo.Description("Enable payment reminders for the Payment Link."),
215 | ),
216 | mcpgo.WithObject(
217 | "notes",
218 | mcpgo.Description("Key-value pairs that can be used to store additional information. Maximum 15 pairs, each value limited to 256 characters."), // nolint:lll
219 | ),
220 | mcpgo.WithString(
221 | "callback_url",
222 | mcpgo.Description("If specified, adds a redirect URL to the Payment Link. Customer will be redirected here after payment."), // nolint:lll
223 | ),
224 | mcpgo.WithString(
225 | "callback_method",
226 | mcpgo.Description("HTTP method for callback redirection. "+
227 | "Must be 'get' if callback_url is set."),
228 | ),
229 | }
230 |
231 | handler := func(
232 | ctx context.Context,
233 | r mcpgo.CallToolRequest,
234 | ) (*mcpgo.ToolResult, error) {
235 | // Create a parameters map to collect validated parameters
236 | upiPlCreateReq := make(map[string]interface{})
237 | customer := make(map[string]interface{})
238 | notify := make(map[string]interface{})
239 | // Validate all parameters with fluent validator
240 | validator := NewValidator(&r).
241 | ValidateAndAddRequiredInt(upiPlCreateReq, "amount").
242 | ValidateAndAddRequiredString(upiPlCreateReq, "currency").
243 | ValidateAndAddOptionalString(upiPlCreateReq, "description").
244 | ValidateAndAddOptionalBool(upiPlCreateReq, "accept_partial").
245 | ValidateAndAddOptionalInt(upiPlCreateReq, "first_min_partial_amount").
246 | ValidateAndAddOptionalInt(upiPlCreateReq, "expire_by").
247 | ValidateAndAddOptionalString(upiPlCreateReq, "reference_id").
248 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
249 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
250 | ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact").
251 | ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
252 | ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email").
253 | ValidateAndAddOptionalBool(upiPlCreateReq, "reminder_enable").
254 | ValidateAndAddOptionalMap(upiPlCreateReq, "notes").
255 | ValidateAndAddOptionalString(upiPlCreateReq, "callback_url").
256 | ValidateAndAddOptionalString(upiPlCreateReq, "callback_method")
257 |
258 | if result, err := validator.HandleErrorsIfAny(); result != nil {
259 | return result, err
260 | }
261 |
262 | // Add the required UPI payment link parameters
263 | upiPlCreateReq["upi_link"] = "true"
264 |
265 | // Handle customer details
266 | if len(customer) > 0 {
267 | upiPlCreateReq["customer"] = customer
268 | }
269 |
270 | // Handle notification settings
271 | if len(notify) > 0 {
272 | upiPlCreateReq["notify"] = notify
273 | }
274 |
275 | client, err := getClientFromContextOrDefault(ctx, client)
276 | if err != nil {
277 | return mcpgo.NewToolResultError(err.Error()), nil
278 | }
279 |
280 | // Create the payment link
281 | paymentLink, err := client.PaymentLink.Create(upiPlCreateReq, nil)
282 | if err != nil {
283 | return mcpgo.NewToolResultError(
284 | fmt.Sprintf("upi pl create failed: %s", err.Error())), nil
285 | }
286 |
287 | return mcpgo.NewToolResultJSON(paymentLink)
288 | }
289 |
290 | return mcpgo.NewTool(
291 | "payment_link_upi_create",
292 | "Create a new UPI payment link in Razorpay with a specified amount and additional options.", // nolint:lll
293 | parameters,
294 | handler,
295 | )
296 | }
297 |
298 | // FetchPaymentLink returns a tool that fetches payment link details using
299 | // payment_link_id
300 | func FetchPaymentLink(
301 | obs *observability.Observability,
302 | client *rzpsdk.Client,
303 | ) mcpgo.Tool {
304 | parameters := []mcpgo.ToolParameter{
305 | mcpgo.WithString(
306 | "payment_link_id",
307 | mcpgo.Description("ID of the payment link to be fetched"+
308 | "(ID should have a plink_ prefix)."),
309 | mcpgo.Required(),
310 | ),
311 | }
312 |
313 | handler := func(
314 | ctx context.Context,
315 | r mcpgo.CallToolRequest,
316 | ) (*mcpgo.ToolResult, error) {
317 | // Get client from context or use default
318 | client, err := getClientFromContextOrDefault(ctx, client)
319 | if err != nil {
320 | return mcpgo.NewToolResultError(err.Error()), nil
321 | }
322 |
323 | fields := make(map[string]interface{})
324 |
325 | validator := NewValidator(&r).
326 | ValidateAndAddRequiredString(fields, "payment_link_id")
327 |
328 | if result, err := validator.HandleErrorsIfAny(); result != nil {
329 | return result, err
330 | }
331 |
332 | paymentLinkId := fields["payment_link_id"].(string)
333 |
334 | paymentLink, err := client.PaymentLink.Fetch(paymentLinkId, nil, nil)
335 | if err != nil {
336 | return mcpgo.NewToolResultError(
337 | fmt.Sprintf("fetching payment link failed: %s", err.Error())), nil
338 | }
339 |
340 | return mcpgo.NewToolResultJSON(paymentLink)
341 | }
342 |
343 | return mcpgo.NewTool(
344 | "fetch_payment_link",
345 | "Fetch payment link details using it's ID. "+
346 | "Response contains the basic details like amount, status etc. "+
347 | "The link could be of any type(standard or UPI)",
348 | parameters,
349 | handler,
350 | )
351 | }
352 |
353 | // ResendPaymentLinkNotification returns a tool that sends/resends notifications
354 | // for a payment link via email or SMS
355 | func ResendPaymentLinkNotification(
356 | obs *observability.Observability,
357 | client *rzpsdk.Client,
358 | ) mcpgo.Tool {
359 | parameters := []mcpgo.ToolParameter{
360 | mcpgo.WithString(
361 | "payment_link_id",
362 | mcpgo.Description("ID of the payment link for which to send notification "+
363 | "(ID should have a plink_ prefix)."), // nolint:lll
364 | mcpgo.Required(),
365 | ),
366 | mcpgo.WithString(
367 | "medium",
368 | mcpgo.Description("Medium through which to send the notification. "+
369 | "Must be either 'sms' or 'email'."), // nolint:lll
370 | mcpgo.Required(),
371 | mcpgo.Enum("sms", "email"),
372 | ),
373 | }
374 |
375 | handler := func(
376 | ctx context.Context,
377 | r mcpgo.CallToolRequest,
378 | ) (*mcpgo.ToolResult, error) {
379 | client, err := getClientFromContextOrDefault(ctx, client)
380 | if err != nil {
381 | return mcpgo.NewToolResultError(err.Error()), nil
382 | }
383 |
384 | fields := make(map[string]interface{})
385 |
386 | validator := NewValidator(&r).
387 | ValidateAndAddRequiredString(fields, "payment_link_id").
388 | ValidateAndAddRequiredString(fields, "medium")
389 |
390 | if result, err := validator.HandleErrorsIfAny(); result != nil {
391 | return result, err
392 | }
393 |
394 | paymentLinkId := fields["payment_link_id"].(string)
395 | medium := fields["medium"].(string)
396 |
397 | // Call the SDK function
398 | response, err := client.PaymentLink.NotifyBy(paymentLinkId, medium, nil, nil)
399 | if err != nil {
400 | return mcpgo.NewToolResultError(
401 | fmt.Sprintf("sending notification failed: %s", err.Error())), nil
402 | }
403 |
404 | return mcpgo.NewToolResultJSON(response)
405 | }
406 |
407 | return mcpgo.NewTool(
408 | "payment_link_notify",
409 | "Send or resend notification for a payment link via SMS or email.", // nolint:lll
410 | parameters,
411 | handler,
412 | )
413 | }
414 |
415 | // UpdatePaymentLink returns a tool that updates an existing payment link
416 | func UpdatePaymentLink(
417 | obs *observability.Observability,
418 | client *rzpsdk.Client,
419 | ) mcpgo.Tool {
420 | parameters := []mcpgo.ToolParameter{
421 | mcpgo.WithString(
422 | "payment_link_id",
423 | mcpgo.Description("ID of the payment link to update "+
424 | "(ID should have a plink_ prefix)."),
425 | mcpgo.Required(),
426 | ),
427 | mcpgo.WithString(
428 | "reference_id",
429 | mcpgo.Description("Adds a unique reference number to the payment link."),
430 | ),
431 | mcpgo.WithNumber(
432 | "expire_by",
433 | mcpgo.Description("Timestamp, in Unix format, when the payment link "+
434 | "should expire."),
435 | ),
436 | mcpgo.WithBoolean(
437 | "reminder_enable",
438 | mcpgo.Description("Enable or disable reminders for the payment link."),
439 | ),
440 | mcpgo.WithBoolean(
441 | "accept_partial",
442 | mcpgo.Description("Allow customers to make partial payments. "+
443 | "Not allowed with UPI payment links."),
444 | ),
445 | mcpgo.WithObject(
446 | "notes",
447 | mcpgo.Description("Key-value pairs for additional information. "+
448 | "Maximum 15 pairs, each value limited to 256 characters."),
449 | ),
450 | }
451 |
452 | handler := func(
453 | ctx context.Context,
454 | r mcpgo.CallToolRequest,
455 | ) (*mcpgo.ToolResult, error) {
456 | client, err := getClientFromContextOrDefault(ctx, client)
457 | if err != nil {
458 | return mcpgo.NewToolResultError(err.Error()), nil
459 | }
460 |
461 | plUpdateReq := make(map[string]interface{})
462 | otherFields := make(map[string]interface{})
463 |
464 | validator := NewValidator(&r).
465 | ValidateAndAddRequiredString(otherFields, "payment_link_id").
466 | ValidateAndAddOptionalString(plUpdateReq, "reference_id").
467 | ValidateAndAddOptionalInt(plUpdateReq, "expire_by").
468 | ValidateAndAddOptionalBool(plUpdateReq, "reminder_enable").
469 | ValidateAndAddOptionalBool(plUpdateReq, "accept_partial").
470 | ValidateAndAddOptionalMap(plUpdateReq, "notes")
471 |
472 | if result, err := validator.HandleErrorsIfAny(); result != nil {
473 | return result, err
474 | }
475 |
476 | paymentLinkId := otherFields["payment_link_id"].(string)
477 |
478 | // Ensure we have at least one field to update
479 | if len(plUpdateReq) == 0 {
480 | return mcpgo.NewToolResultError(
481 | "at least one field to update must be provided"), nil
482 | }
483 |
484 | // Call the SDK function
485 | paymentLink, err := client.PaymentLink.Update(paymentLinkId, plUpdateReq, nil)
486 | if err != nil {
487 | return mcpgo.NewToolResultError(
488 | fmt.Sprintf("updating payment link failed: %s", err.Error())), nil
489 | }
490 |
491 | return mcpgo.NewToolResultJSON(paymentLink)
492 | }
493 |
494 | return mcpgo.NewTool(
495 | "update_payment_link",
496 | "Update any existing standard or UPI payment link with new details such as reference ID, "+ // nolint:lll
497 | "expiry date, or notes.",
498 | parameters,
499 | handler,
500 | )
501 | }
502 |
503 | // FetchAllPaymentLinks returns a tool that fetches all payment links
504 | // with optional filtering
505 | func FetchAllPaymentLinks(
506 | obs *observability.Observability,
507 | client *rzpsdk.Client,
508 | ) mcpgo.Tool {
509 | parameters := []mcpgo.ToolParameter{
510 | mcpgo.WithString(
511 | "payment_id",
512 | mcpgo.Description("Optional: Filter by payment ID associated with payment links"), // nolint:lll
513 | ),
514 | mcpgo.WithString(
515 | "reference_id",
516 | mcpgo.Description("Optional: Filter by reference ID used when creating payment links"), // nolint:lll
517 | ),
518 | mcpgo.WithNumber(
519 | "upi_link",
520 | mcpgo.Description("Optional: Filter only upi links. "+
521 | "Value should be 1 if you want only upi links, 0 for only standard links"+
522 | "If not provided, all types of links will be returned"),
523 | ),
524 | }
525 |
526 | handler := func(
527 | ctx context.Context,
528 | r mcpgo.CallToolRequest,
529 | ) (*mcpgo.ToolResult, error) {
530 | client, err := getClientFromContextOrDefault(ctx, client)
531 | if err != nil {
532 | return mcpgo.NewToolResultError(err.Error()), nil
533 | }
534 |
535 | plListReq := make(map[string]interface{})
536 |
537 | validator := NewValidator(&r).
538 | ValidateAndAddOptionalString(plListReq, "payment_id").
539 | ValidateAndAddOptionalString(plListReq, "reference_id").
540 | ValidateAndAddOptionalInt(plListReq, "upi_link")
541 |
542 | if result, err := validator.HandleErrorsIfAny(); result != nil {
543 | return result, err
544 | }
545 |
546 | // Call the API directly using the Request object
547 | response, err := client.PaymentLink.All(plListReq, nil)
548 | if err != nil {
549 | return mcpgo.NewToolResultError(
550 | fmt.Sprintf("fetching payment links failed: %s", err.Error())), nil
551 | }
552 |
553 | return mcpgo.NewToolResultJSON(response)
554 | }
555 |
556 | return mcpgo.NewTool(
557 | "fetch_all_payment_links",
558 | "Fetch all payment links with optional filtering by payment ID or reference ID."+ // nolint:lll
559 | "You can specify the upi_link parameter to filter by link type.",
560 | parameters,
561 | handler,
562 | )
563 | }
564 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/refunds_test.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/razorpay/razorpay-go/constants"
10 |
11 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
12 | )
13 |
14 | func Test_CreateRefund(t *testing.T) {
15 | createRefundPathFmt := fmt.Sprintf(
16 | "/%s%s/%%s/refund",
17 | constants.VERSION_V1,
18 | constants.PAYMENT_URL,
19 | )
20 |
21 | // Define test responses
22 | successfulRefundResp := map[string]interface{}{
23 | "id": "rfnd_FP8QHiV938haTz",
24 | "entity": "refund",
25 | "amount": float64(500100),
26 | "currency": "INR",
27 | "payment_id": "pay_29QQoUBi66xm2f",
28 | "notes": map[string]interface{}{},
29 | "receipt": "Receipt No. 31",
30 | "acquirer_data": map[string]interface{}{"arn": nil},
31 | "created_at": float64(1597078866),
32 | "batch_id": nil,
33 | "status": "processed",
34 | "speed_processed": "normal",
35 | "speed_requested": "normal",
36 | }
37 |
38 | errorResp := map[string]interface{}{
39 | "error": map[string]interface{}{
40 | "code": "BAD_REQUEST_ERROR",
41 | "description": "Razorpay API error: Bad request",
42 | },
43 | }
44 |
45 | tests := []RazorpayToolTestCase{
46 | {
47 | Name: "successful full refund",
48 | Request: map[string]interface{}{
49 | "payment_id": "pay_29QQoUBi66xm2f",
50 | "amount": float64(500100),
51 | "receipt": "Receipt No. 31",
52 | },
53 | MockHttpClient: func() (*http.Client, *httptest.Server) {
54 | return mock.NewHTTPClient(
55 | mock.Endpoint{
56 | Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
57 | Method: "POST",
58 | Response: successfulRefundResp,
59 | },
60 | )
61 | },
62 | ExpectError: false,
63 | ExpectedResult: successfulRefundResp,
64 | },
65 | {
66 | Name: "refund with speed parameter",
67 | Request: map[string]interface{}{
68 | "payment_id": "pay_29QQoUBi66xm2f",
69 | "amount": float64(500100),
70 | "speed": "optimum",
71 | },
72 | MockHttpClient: func() (*http.Client, *httptest.Server) {
73 | speedRefundResp := map[string]interface{}{
74 | "id": "rfnd_HzAbPEkKtRq48V",
75 | "entity": "refund",
76 | "amount": float64(500100),
77 | "payment_id": "pay_29QQoUBi66xm2f",
78 | "status": "processed",
79 | "speed_processed": "instant",
80 | "speed_requested": "optimum",
81 | }
82 | return mock.NewHTTPClient(
83 | mock.Endpoint{
84 | Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
85 | Method: "POST",
86 | Response: speedRefundResp,
87 | },
88 | )
89 | },
90 | ExpectError: false,
91 | ExpectedResult: map[string]interface{}{
92 | "id": "rfnd_HzAbPEkKtRq48V",
93 | "entity": "refund",
94 | "amount": float64(500100),
95 | "payment_id": "pay_29QQoUBi66xm2f",
96 | "status": "processed",
97 | "speed_processed": "instant",
98 | "speed_requested": "optimum",
99 | },
100 | },
101 | {
102 | Name: "refund API server error",
103 | Request: map[string]interface{}{
104 | "payment_id": "pay_29QQoUBi66xm2f",
105 | "amount": float64(500100),
106 | },
107 | MockHttpClient: func() (*http.Client, *httptest.Server) {
108 | return mock.NewHTTPClient(
109 | mock.Endpoint{
110 | Path: fmt.Sprintf(createRefundPathFmt, "pay_29QQoUBi66xm2f"),
111 | Method: "POST",
112 | Response: errorResp,
113 | },
114 | )
115 | },
116 | ExpectError: true,
117 | ExpectedErrMsg: "creating refund failed: Razorpay API error: Bad request",
118 | },
119 | {
120 | Name: "multiple validation errors",
121 | Request: map[string]interface{}{
122 | // Missing payment_id parameter
123 | "amount": "not-a-number", // Wrong type for amount
124 | "speed": 12345, // Wrong type for speed
125 | "notes": "not-an-object", // Wrong type for notes
126 | },
127 | MockHttpClient: nil,
128 | ExpectError: true,
129 | ExpectedErrMsg: "Validation errors:\n- " +
130 | "missing required parameter: payment_id\n- " +
131 | "invalid parameter type: amount\n- " +
132 | "invalid parameter type: speed\n- " +
133 | "invalid parameter type: notes",
134 | },
135 | }
136 |
137 | for _, tc := range tests {
138 | t.Run(tc.Name, func(t *testing.T) {
139 | runToolTest(t, tc, CreateRefund, "Refund")
140 | })
141 | }
142 | }
143 |
144 | func Test_FetchRefund(t *testing.T) {
145 | fetchRefundPathFmt := fmt.Sprintf(
146 | "/%s%s/%%s",
147 | constants.VERSION_V1,
148 | constants.REFUND_URL,
149 | )
150 |
151 | // Define test response for successful refund fetch
152 | successfulRefundResp := map[string]interface{}{
153 | "id": "rfnd_DfjjhJC6eDvUAi",
154 | "entity": "refund",
155 | "amount": float64(6000),
156 | "currency": "INR",
157 | "payment_id": "pay_EpkFDYRirena0f",
158 | "notes": map[string]interface{}{
159 | "comment": "Issuing an instant refund",
160 | },
161 | "receipt": nil,
162 | "acquirer_data": map[string]interface{}{
163 | "arn": "10000000000000",
164 | },
165 | "created_at": float64(1589521675),
166 | "batch_id": nil,
167 | "status": "processed",
168 | "speed_processed": "optimum",
169 | "speed_requested": "optimum",
170 | }
171 |
172 | // Define error responses
173 | notFoundResp := map[string]interface{}{
174 | "error": map[string]interface{}{
175 | "code": "BAD_REQUEST_ERROR",
176 | "description": "The id provided does not exist",
177 | },
178 | }
179 |
180 | tests := []RazorpayToolTestCase{
181 | {
182 | Name: "successful refund fetch",
183 | Request: map[string]interface{}{
184 | "refund_id": "rfnd_DfjjhJC6eDvUAi",
185 | },
186 | MockHttpClient: func() (*http.Client, *httptest.Server) {
187 | return mock.NewHTTPClient(
188 | mock.Endpoint{
189 | Path: fmt.Sprintf(fetchRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
190 | Method: "GET",
191 | Response: successfulRefundResp,
192 | },
193 | )
194 | },
195 | ExpectError: false,
196 | ExpectedResult: successfulRefundResp,
197 | },
198 | {
199 | Name: "refund id not found",
200 | Request: map[string]interface{}{
201 | "refund_id": "rfnd_nonexistent",
202 | },
203 | MockHttpClient: func() (*http.Client, *httptest.Server) {
204 | return mock.NewHTTPClient(
205 | mock.Endpoint{
206 | Path: fmt.Sprintf(fetchRefundPathFmt, "rfnd_nonexistent"),
207 | Method: "GET",
208 | Response: notFoundResp,
209 | },
210 | )
211 | },
212 | ExpectError: true,
213 | ExpectedErrMsg: "fetching refund failed: The id provided does not exist",
214 | },
215 | {
216 | Name: "missing refund_id parameter",
217 | Request: map[string]interface{}{},
218 | MockHttpClient: nil,
219 | ExpectError: true,
220 | ExpectedErrMsg: "missing required parameter: refund_id",
221 | },
222 | {
223 | Name: "multiple validation errors",
224 | Request: map[string]interface{}{
225 | // Missing refund_id parameter
226 | "non_existent_param": 12345, // Additional parameter that doesn't exist
227 | },
228 | MockHttpClient: nil,
229 | ExpectError: true,
230 | ExpectedErrMsg: "missing required parameter: refund_id",
231 | },
232 | }
233 |
234 | for _, tc := range tests {
235 | t.Run(tc.Name, func(t *testing.T) {
236 | runToolTest(t, tc, FetchRefund, "Refund")
237 | })
238 | }
239 | }
240 |
241 | func Test_UpdateRefund(t *testing.T) {
242 | updateRefundPathFmt := fmt.Sprintf(
243 | "/%s%s/%%s",
244 | constants.VERSION_V1,
245 | constants.REFUND_URL,
246 | )
247 |
248 | // Define test response for successful refund update
249 | successfulUpdateResp := map[string]interface{}{
250 | "id": "rfnd_DfjjhJC6eDvUAi",
251 | "entity": "refund",
252 | "amount": float64(300100),
253 | "currency": "INR",
254 | "payment_id": "pay_FIKOnlyii5QGNx",
255 | "notes": map[string]interface{}{
256 | "notes_key_1": "Beam me up Scotty.",
257 | "notes_key_2": "Engage",
258 | },
259 | "receipt": nil,
260 | "acquirer_data": map[string]interface{}{"arn": "10000000000000"},
261 | "created_at": float64(1597078124),
262 | "batch_id": nil,
263 | "status": "processed",
264 | "speed_processed": "normal",
265 | "speed_requested": "optimum",
266 | }
267 |
268 | // Define error responses
269 | notFoundResp := map[string]interface{}{
270 | "error": map[string]interface{}{
271 | "code": "BAD_REQUEST_ERROR",
272 | "description": "The id provided does not exist",
273 | },
274 | }
275 |
276 | tests := []RazorpayToolTestCase{
277 | {
278 | Name: "successful refund update",
279 | Request: map[string]interface{}{
280 | "refund_id": "rfnd_DfjjhJC6eDvUAi",
281 | "notes": map[string]interface{}{
282 | "notes_key_1": "Beam me up Scotty.",
283 | "notes_key_2": "Engage",
284 | },
285 | },
286 | MockHttpClient: func() (*http.Client, *httptest.Server) {
287 | return mock.NewHTTPClient(
288 | mock.Endpoint{
289 | Path: fmt.Sprintf(updateRefundPathFmt, "rfnd_DfjjhJC6eDvUAi"),
290 | Method: "PATCH",
291 | Response: successfulUpdateResp,
292 | },
293 | )
294 | },
295 | ExpectError: false,
296 | ExpectedResult: successfulUpdateResp,
297 | },
298 | {
299 | Name: "refund id not found",
300 | Request: map[string]interface{}{
301 | "refund_id": "rfnd_nonexistent",
302 | "notes": map[string]interface{}{
303 | "note_key": "Test note",
304 | },
305 | },
306 | MockHttpClient: func() (*http.Client, *httptest.Server) {
307 | return mock.NewHTTPClient(
308 | mock.Endpoint{
309 | Path: fmt.Sprintf(updateRefundPathFmt, "rfnd_nonexistent"),
310 | Method: "PATCH",
311 | Response: notFoundResp,
312 | },
313 | )
314 | },
315 | ExpectError: true,
316 | ExpectedErrMsg: "updating refund failed: The id provided does not exist",
317 | },
318 | {
319 | Name: "missing refund_id parameter",
320 | Request: map[string]interface{}{},
321 | MockHttpClient: nil,
322 | ExpectError: true,
323 | ExpectedErrMsg: "missing required parameter: refund_id",
324 | },
325 | {
326 | Name: "missing notes parameter",
327 | Request: map[string]interface{}{
328 | "refund_id": "rfnd_DfjjhJC6eDvUAi",
329 | },
330 | MockHttpClient: nil,
331 | ExpectError: true,
332 | ExpectedErrMsg: "missing required parameter: notes",
333 | },
334 | {
335 | Name: "multiple validation errors",
336 | Request: map[string]interface{}{
337 | // Missing both refund_id and notes parameters
338 | "non_existent_param": 12345, // Additional parameter that doesn't exist
339 | },
340 | MockHttpClient: nil,
341 | ExpectError: true,
342 | ExpectedErrMsg: "Validation errors:\n- " +
343 | "missing required parameter: refund_id\n- " +
344 | "missing required parameter: notes",
345 | },
346 | }
347 |
348 | for _, tc := range tests {
349 | t.Run(tc.Name, func(t *testing.T) {
350 | runToolTest(t, tc, UpdateRefund, "Refund")
351 | })
352 | }
353 | }
354 |
355 | func Test_FetchMultipleRefundsForPayment(t *testing.T) {
356 | fetchMultipleRefundsPathFmt := fmt.Sprintf(
357 | "/%s%s/%%s/refunds",
358 | constants.VERSION_V1,
359 | constants.PAYMENT_URL,
360 | )
361 |
362 | // Define test response for successful multiple refunds fetch
363 | successfulMultipleRefundsResp := map[string]interface{}{
364 | "entity": "collection",
365 | "count": float64(2),
366 | "items": []interface{}{
367 | map[string]interface{}{
368 | "id": "rfnd_FP8DDKxqJif6ca",
369 | "entity": "refund",
370 | "amount": float64(300100),
371 | "currency": "INR",
372 | "payment_id": "pay_29QQoUBi66xm2f",
373 | "notes": map[string]interface{}{
374 | "comment": "Comment for refund",
375 | },
376 | "receipt": nil,
377 | "acquirer_data": map[string]interface{}{
378 | "arn": "10000000000000",
379 | },
380 | "created_at": float64(1597078124),
381 | "batch_id": nil,
382 | "status": "processed",
383 | "speed_processed": "normal",
384 | "speed_requested": "optimum",
385 | },
386 | map[string]interface{}{
387 | "id": "rfnd_FP8DRfu3ygfOaC",
388 | "entity": "refund",
389 | "amount": float64(200000),
390 | "currency": "INR",
391 | "payment_id": "pay_29QQoUBi66xm2f",
392 | "notes": map[string]interface{}{
393 | "comment": "Comment for refund",
394 | },
395 | "receipt": nil,
396 | "acquirer_data": map[string]interface{}{
397 | "arn": "10000000000000",
398 | },
399 | "created_at": float64(1597078137),
400 | "batch_id": nil,
401 | "status": "processed",
402 | "speed_processed": "normal",
403 | "speed_requested": "optimum",
404 | },
405 | },
406 | }
407 |
408 | // Define error responses
409 | errorResp := map[string]interface{}{
410 | "error": map[string]interface{}{
411 | "code": "BAD_REQUEST_ERROR",
412 | "description": "Bad request",
413 | },
414 | }
415 |
416 | tests := []RazorpayToolTestCase{
417 | {
418 | Name: "fetch multiple refunds with query params",
419 | Request: map[string]interface{}{
420 | "payment_id": "pay_29QQoUBi66xm2f",
421 | "from": 1500826740,
422 | "to": 1500826760,
423 | "count": 10,
424 | "skip": 0,
425 | },
426 | MockHttpClient: func() (*http.Client, *httptest.Server) {
427 | return mock.NewHTTPClient(
428 | mock.Endpoint{
429 | Path: fmt.Sprintf(
430 | fetchMultipleRefundsPathFmt,
431 | "pay_29QQoUBi66xm2f",
432 | ),
433 | Method: "GET",
434 | Response: successfulMultipleRefundsResp,
435 | },
436 | )
437 | },
438 | ExpectError: false,
439 | ExpectedResult: successfulMultipleRefundsResp,
440 | },
441 | {
442 | Name: "fetch multiple refunds api error",
443 | Request: map[string]interface{}{
444 | "payment_id": "pay_invalid",
445 | },
446 | MockHttpClient: func() (*http.Client, *httptest.Server) {
447 | return mock.NewHTTPClient(
448 | mock.Endpoint{
449 | Path: fmt.Sprintf(
450 | fetchMultipleRefundsPathFmt,
451 | "pay_invalid",
452 | ),
453 | Method: "GET",
454 | Response: errorResp,
455 | },
456 | )
457 | },
458 | ExpectError: true,
459 | ExpectedErrMsg: "fetching multiple refunds failed: Bad request",
460 | },
461 | {
462 | Name: "missing payment_id parameter",
463 | Request: map[string]interface{}{},
464 | MockHttpClient: nil,
465 | ExpectError: true,
466 | ExpectedErrMsg: "missing required parameter: payment_id",
467 | },
468 | {
469 | Name: "multiple validation errors",
470 | Request: map[string]interface{}{
471 | // Missing payment_id parameter
472 | "from": "not-a-number", // Wrong type for from
473 | "to": "not-a-number", // Wrong type for to
474 | "count": "not-a-number", // Wrong type for count
475 | "skip": "not-a-number", // Wrong type for skip
476 | },
477 | MockHttpClient: nil,
478 | ExpectError: true,
479 | ExpectedErrMsg: "Validation errors:\n- " +
480 | "missing required parameter: payment_id\n- " +
481 | "invalid parameter type: from\n- " +
482 | "invalid parameter type: to\n- " +
483 | "invalid parameter type: count\n- " +
484 | "invalid parameter type: skip",
485 | },
486 | }
487 |
488 | for _, tc := range tests {
489 | t.Run(tc.Name, func(t *testing.T) {
490 | runToolTest(t, tc, FetchMultipleRefundsForPayment, "Refund")
491 | })
492 | }
493 | }
494 |
495 | func Test_FetchSpecificRefundForPayment(t *testing.T) {
496 | fetchSpecificRefundPathFmt := fmt.Sprintf(
497 | "/%s%s/%%s/refunds/%%s",
498 | constants.VERSION_V1,
499 | constants.PAYMENT_URL,
500 | )
501 |
502 | // Define test response for successful specific refund fetch
503 | successfulSpecificRefundResp := map[string]interface{}{
504 | "id": "rfnd_AABBdHIieexn5c",
505 | "entity": "refund",
506 | "amount": float64(300100),
507 | "currency": "INR",
508 | "payment_id": "pay_FIKOnlyii5QGNx",
509 | "notes": map[string]interface{}{
510 | "comment": "Comment for refund",
511 | },
512 | "receipt": nil,
513 | "acquirer_data": map[string]interface{}{"arn": "10000000000000"},
514 | "created_at": float64(1597078124),
515 | "batch_id": nil,
516 | "status": "processed",
517 | "speed_processed": "normal",
518 | "speed_requested": "optimum",
519 | }
520 |
521 | // Define error responses
522 | notFoundResp := map[string]interface{}{
523 | "error": map[string]interface{}{
524 | "code": "BAD_REQUEST_ERROR",
525 | "description": "The id provided does not exist",
526 | },
527 | }
528 |
529 | tests := []RazorpayToolTestCase{
530 | {
531 | Name: "successful specific refund fetch",
532 | Request: map[string]interface{}{
533 | "payment_id": "pay_FIKOnlyii5QGNx",
534 | "refund_id": "rfnd_AABBdHIieexn5c",
535 | },
536 | MockHttpClient: func() (*http.Client, *httptest.Server) {
537 | return mock.NewHTTPClient(
538 | mock.Endpoint{
539 | Path: fmt.Sprintf(
540 | fetchSpecificRefundPathFmt,
541 | "pay_FIKOnlyii5QGNx",
542 | "rfnd_AABBdHIieexn5c",
543 | ),
544 | Method: "GET",
545 | Response: successfulSpecificRefundResp,
546 | },
547 | )
548 | },
549 | ExpectError: false,
550 | ExpectedResult: successfulSpecificRefundResp,
551 | },
552 | {
553 | Name: "refund id not found",
554 | Request: map[string]interface{}{
555 | "payment_id": "pay_FIKOnlyii5QGNx",
556 | "refund_id": "rfnd_nonexistent",
557 | },
558 | MockHttpClient: func() (*http.Client, *httptest.Server) {
559 | return mock.NewHTTPClient(
560 | mock.Endpoint{
561 | Path: fmt.Sprintf(
562 | fetchSpecificRefundPathFmt,
563 | "pay_FIKOnlyii5QGNx",
564 | "rfnd_nonexistent",
565 | ),
566 | Method: "GET",
567 | Response: notFoundResp,
568 | },
569 | )
570 | },
571 | ExpectError: true,
572 | ExpectedErrMsg: "fetching specific refund for payment failed: " +
573 | "The id provided does not exist",
574 | },
575 | {
576 | Name: "missing payment_id parameter",
577 | Request: map[string]interface{}{
578 | "refund_id": "rfnd_AABBdHIieexn5c",
579 | },
580 | MockHttpClient: nil,
581 | ExpectError: true,
582 | ExpectedErrMsg: "missing required parameter: payment_id",
583 | },
584 | {
585 | Name: "missing refund_id parameter",
586 | Request: map[string]interface{}{
587 | "payment_id": "pay_FIKOnlyii5QGNx",
588 | },
589 | MockHttpClient: nil,
590 | ExpectError: true,
591 | ExpectedErrMsg: "missing required parameter: refund_id",
592 | },
593 | {
594 | Name: "multiple validation errors",
595 | Request: map[string]interface{}{
596 | // Missing both payment_id and refund_id parameters
597 | "non_existent_param": 12345, // Additional parameter that doesn't exist
598 | },
599 | MockHttpClient: nil,
600 | ExpectError: true,
601 | ExpectedErrMsg: "Validation errors:\n- " +
602 | "missing required parameter: payment_id\n- " +
603 | "missing required parameter: refund_id",
604 | },
605 | }
606 |
607 | for _, tc := range tests {
608 | t.Run(tc.Name, func(t *testing.T) {
609 | runToolTest(t, tc, FetchSpecificRefundForPayment, "Refund")
610 | })
611 | }
612 | }
613 |
614 | func Test_FetchAllRefunds(t *testing.T) {
615 | fetchAllRefundsPath := fmt.Sprintf(
616 | "/%s%s",
617 | constants.VERSION_V1,
618 | constants.REFUND_URL,
619 | )
620 |
621 | // Define test response for successful refund fetch
622 | successfulRefundsResp := map[string]interface{}{
623 | "entity": "collection",
624 | "count": float64(2),
625 | "items": []interface{}{
626 | map[string]interface{}{
627 | "id": "rfnd_FFX6AnnIN3puqW",
628 | "entity": "refund",
629 | "amount": float64(88800),
630 | "currency": "INR",
631 | "payment_id": "pay_FFX5FdEYx8jPwA",
632 | "notes": map[string]interface{}{
633 | "comment": "Issuing an instant refund",
634 | },
635 | "receipt": nil,
636 | "acquirer_data": map[string]interface{}{},
637 | "created_at": float64(1594982363),
638 | "batch_id": nil,
639 | "status": "processed",
640 | "speed_processed": "optimum",
641 | "speed_requested": "optimum",
642 | },
643 | map[string]interface{}{
644 | "id": "rfnd_EqWThTE7dd7utf",
645 | "entity": "refund",
646 | "amount": float64(6000),
647 | "currency": "INR",
648 | "payment_id": "pay_EpkFDYRirena0f",
649 | "notes": map[string]interface{}{
650 | "comment": "Issuing a normal refund",
651 | },
652 | "receipt": nil,
653 | "acquirer_data": map[string]interface{}{
654 | "arn": "10000000000000",
655 | },
656 | "created_at": float64(1589521675),
657 | "batch_id": nil,
658 | "status": "processed",
659 | "speed_processed": "normal",
660 | "speed_requested": "normal",
661 | },
662 | },
663 | }
664 |
665 | // Define error response
666 | errorResp := map[string]interface{}{
667 | "error": map[string]interface{}{
668 | "code": "BAD_REQUEST_ERROR",
669 | "description": "Bad request",
670 | },
671 | }
672 |
673 | tests := []RazorpayToolTestCase{
674 | {
675 | Name: "successful fetch with pagination parameters",
676 | Request: map[string]interface{}{
677 | "count": 2,
678 | "skip": 1,
679 | "from": 1589000000,
680 | "to": 1595000000,
681 | },
682 | MockHttpClient: func() (*http.Client, *httptest.Server) {
683 | return mock.NewHTTPClient(
684 | mock.Endpoint{
685 | Path: fetchAllRefundsPath,
686 | Method: "GET",
687 | Response: successfulRefundsResp,
688 | },
689 | )
690 | },
691 | ExpectError: false,
692 | ExpectedResult: successfulRefundsResp,
693 | },
694 | {
695 | Name: "fetch with API error",
696 | Request: map[string]interface{}{},
697 | MockHttpClient: func() (*http.Client, *httptest.Server) {
698 | return mock.NewHTTPClient(
699 | mock.Endpoint{
700 | Path: fetchAllRefundsPath,
701 | Method: "GET",
702 | Response: errorResp,
703 | },
704 | )
705 | },
706 | ExpectError: true,
707 | ExpectedErrMsg: "fetching refunds failed",
708 | },
709 | {
710 | Name: "multiple validation errors",
711 | Request: map[string]interface{}{
712 | "from": "not-a-number", // Wrong type for from
713 | "to": "not-a-number", // Wrong type for to
714 | "count": "not-a-number", // Wrong type for count
715 | "skip": "not-a-number", // Wrong type for skip
716 | },
717 | MockHttpClient: nil,
718 | ExpectError: true,
719 | ExpectedErrMsg: "Validation errors:\n- " +
720 | "invalid parameter type: from\n- " +
721 | "invalid parameter type: to\n- " +
722 | "invalid parameter type: count\n- " +
723 | "invalid parameter type: skip",
724 | },
725 | }
726 |
727 | for _, tc := range tests {
728 | t.Run(tc.Name, func(t *testing.T) {
729 | runToolTest(t, tc, FetchAllRefunds, "Refund")
730 | })
731 | }
732 | }
733 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/settlements_test.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/razorpay/razorpay-go/constants"
10 |
11 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
12 | )
13 |
14 | func Test_FetchSettlement(t *testing.T) {
15 | fetchSettlementPathFmt := fmt.Sprintf(
16 | "/%s%s/%%s",
17 | constants.VERSION_V1,
18 | constants.SETTLEMENT_URL,
19 | )
20 |
21 | settlementResp := map[string]interface{}{
22 | "id": "setl_FNj7g2YS5J67Rz",
23 | "entity": "settlement",
24 | "amount": float64(9973635),
25 | "status": "processed",
26 | "fees": float64(471),
27 | "tax": float64(72),
28 | "utr": "1568176198",
29 | "created_at": float64(1568176198),
30 | }
31 |
32 | settlementNotFoundResp := map[string]interface{}{
33 | "error": map[string]interface{}{
34 | "code": "BAD_REQUEST_ERROR",
35 | "description": "settlement not found",
36 | },
37 | }
38 |
39 | tests := []RazorpayToolTestCase{
40 | {
41 | Name: "successful settlement fetch",
42 | Request: map[string]interface{}{
43 | "settlement_id": "setl_FNj7g2YS5J67Rz",
44 | },
45 | MockHttpClient: func() (*http.Client, *httptest.Server) {
46 | return mock.NewHTTPClient(
47 | mock.Endpoint{
48 | Path: fmt.Sprintf(fetchSettlementPathFmt, "setl_FNj7g2YS5J67Rz"),
49 | Method: "GET",
50 | Response: settlementResp,
51 | },
52 | )
53 | },
54 | ExpectError: false,
55 | ExpectedResult: settlementResp,
56 | },
57 | {
58 | Name: "settlement not found",
59 | Request: map[string]interface{}{
60 | "settlement_id": "setl_invalid",
61 | },
62 | MockHttpClient: func() (*http.Client, *httptest.Server) {
63 | return mock.NewHTTPClient(
64 | mock.Endpoint{
65 | Path: fmt.Sprintf(fetchSettlementPathFmt, "setl_invalid"),
66 | Method: "GET",
67 | Response: settlementNotFoundResp,
68 | },
69 | )
70 | },
71 | ExpectError: true,
72 | ExpectedErrMsg: "fetching settlement failed: settlement not found",
73 | },
74 | {
75 | Name: "missing settlement_id parameter",
76 | Request: map[string]interface{}{},
77 | MockHttpClient: nil, // No HTTP client needed for validation error
78 | ExpectError: true,
79 | ExpectedErrMsg: "missing required parameter: settlement_id",
80 | },
81 | }
82 |
83 | for _, tc := range tests {
84 | t.Run(tc.Name, func(t *testing.T) {
85 | runToolTest(t, tc, FetchSettlement, "Settlement")
86 | })
87 | }
88 | }
89 |
90 | func Test_FetchSettlementRecon(t *testing.T) {
91 | fetchSettlementReconPath := fmt.Sprintf(
92 | "/%s%s/recon/combined",
93 | constants.VERSION_V1,
94 | constants.SETTLEMENT_URL,
95 | )
96 |
97 | settlementReconResp := map[string]interface{}{
98 | "entity": "collection",
99 | "count": float64(1),
100 | "items": []interface{}{
101 | map[string]interface{}{
102 | "entity": "settlement",
103 | "settlement_id": "setl_FNj7g2YS5J67Rz",
104 | "settlement_utr": "1568176198",
105 | "amount": float64(9973635),
106 | "settlement_type": "regular",
107 | "settlement_status": "processed",
108 | "created_at": float64(1568176198),
109 | },
110 | },
111 | }
112 |
113 | invalidParamsResp := map[string]interface{}{
114 | "error": map[string]interface{}{
115 | "code": "BAD_REQUEST_ERROR",
116 | "description": "missing required parameters",
117 | },
118 | }
119 |
120 | tests := []RazorpayToolTestCase{
121 | {
122 | Name: "successful settlement reconciliation fetch",
123 | Request: map[string]interface{}{
124 | "year": float64(2022),
125 | "month": float64(10),
126 | "day": float64(15),
127 | "count": float64(10),
128 | "skip": float64(0),
129 | },
130 | MockHttpClient: func() (*http.Client, *httptest.Server) {
131 | return mock.NewHTTPClient(
132 | mock.Endpoint{
133 | Path: fetchSettlementReconPath,
134 | Method: "GET",
135 | Response: settlementReconResp,
136 | },
137 | )
138 | },
139 | ExpectError: false,
140 | ExpectedResult: settlementReconResp,
141 | },
142 | {
143 | Name: "settlement reconciliation with required params only",
144 | Request: map[string]interface{}{
145 | "year": float64(2022),
146 | "month": float64(10),
147 | },
148 | MockHttpClient: func() (*http.Client, *httptest.Server) {
149 | return mock.NewHTTPClient(
150 | mock.Endpoint{
151 | Path: fetchSettlementReconPath,
152 | Method: "GET",
153 | Response: settlementReconResp,
154 | },
155 | )
156 | },
157 | ExpectError: false,
158 | ExpectedResult: settlementReconResp,
159 | },
160 | {
161 | Name: "settlement reconciliation with invalid params",
162 | Request: map[string]interface{}{
163 | "year": float64(2022),
164 | // missing month parameter
165 | },
166 | MockHttpClient: func() (*http.Client, *httptest.Server) {
167 | return mock.NewHTTPClient(
168 | mock.Endpoint{
169 | Path: fetchSettlementReconPath,
170 | Method: "GET",
171 | Response: invalidParamsResp,
172 | },
173 | )
174 | },
175 | ExpectError: true,
176 | ExpectedErrMsg: "missing required parameter: month",
177 | },
178 | {
179 | Name: "missing required parameters",
180 | Request: map[string]interface{}{},
181 | MockHttpClient: nil, // No HTTP client needed for validation error
182 | ExpectError: true,
183 | ExpectedErrMsg: "missing required parameter: year",
184 | },
185 | }
186 |
187 | for _, tc := range tests {
188 | t.Run(tc.Name, func(t *testing.T) {
189 | runToolTest(t, tc, FetchSettlementRecon, "Settlement Reconciliation")
190 | })
191 | }
192 | }
193 |
194 | func Test_FetchAllSettlements(t *testing.T) {
195 | fetchAllSettlementsPath := fmt.Sprintf(
196 | "/%s%s",
197 | constants.VERSION_V1,
198 | constants.SETTLEMENT_URL,
199 | )
200 |
201 | // Define the sample response for all settlements
202 | settlementsResp := map[string]interface{}{
203 | "entity": "collection",
204 | "count": float64(2),
205 | "items": []interface{}{
206 | map[string]interface{}{
207 | "id": "setl_FNj7g2YS5J67Rz",
208 | "entity": "settlement",
209 | "amount": float64(9973635),
210 | "status": "processed",
211 | },
212 | map[string]interface{}{
213 | "id": "setl_FJOp0jOWlalIvt",
214 | "entity": "settlement",
215 | "amount": float64(299114),
216 | "status": "processed",
217 | },
218 | },
219 | }
220 |
221 | invalidParamsResp := map[string]interface{}{
222 | "error": map[string]interface{}{
223 | "code": "BAD_REQUEST_ERROR",
224 | "description": "from must be between 946684800 and 4765046400",
225 | },
226 | }
227 |
228 | tests := []RazorpayToolTestCase{
229 | {
230 | Name: "successful settlements fetch with no parameters",
231 | Request: map[string]interface{}{},
232 | MockHttpClient: func() (*http.Client, *httptest.Server) {
233 | return mock.NewHTTPClient(
234 | mock.Endpoint{
235 | Path: fetchAllSettlementsPath,
236 | Method: "GET",
237 | Response: settlementsResp,
238 | },
239 | )
240 | },
241 | ExpectError: false,
242 | ExpectedResult: settlementsResp,
243 | },
244 | {
245 | Name: "successful settlements fetch with pagination",
246 | Request: map[string]interface{}{
247 | "count": float64(10),
248 | "skip": float64(0),
249 | },
250 | MockHttpClient: func() (*http.Client, *httptest.Server) {
251 | return mock.NewHTTPClient(
252 | mock.Endpoint{
253 | Path: fetchAllSettlementsPath,
254 | Method: "GET",
255 | Response: settlementsResp,
256 | },
257 | )
258 | },
259 | ExpectError: false,
260 | ExpectedResult: settlementsResp,
261 | },
262 | {
263 | Name: "successful settlements fetch with date range",
264 | Request: map[string]interface{}{
265 | "from": float64(1609459200), // 2021-01-01
266 | "to": float64(1640995199), // 2021-12-31
267 | },
268 | MockHttpClient: func() (*http.Client, *httptest.Server) {
269 | return mock.NewHTTPClient(
270 | mock.Endpoint{
271 | Path: fetchAllSettlementsPath,
272 | Method: "GET",
273 | Response: settlementsResp,
274 | },
275 | )
276 | },
277 | ExpectError: false,
278 | ExpectedResult: settlementsResp,
279 | },
280 | {
281 | Name: "settlements fetch with invalid timestamp",
282 | Request: map[string]interface{}{
283 | "from": float64(900000000), // Invalid timestamp (too early)
284 | "to": float64(1600000000),
285 | },
286 | MockHttpClient: func() (*http.Client, *httptest.Server) {
287 | return mock.NewHTTPClient(
288 | mock.Endpoint{
289 | Path: fetchAllSettlementsPath,
290 | Method: "GET",
291 | Response: invalidParamsResp,
292 | },
293 | )
294 | },
295 | ExpectError: true,
296 | ExpectedErrMsg: "fetching settlements failed: from must be " +
297 | "between 946684800 and 4765046400",
298 | },
299 | }
300 |
301 | for _, tc := range tests {
302 | t.Run(tc.Name, func(t *testing.T) {
303 | runToolTest(t, tc, FetchAllSettlements, "Settlements List")
304 | })
305 | }
306 | }
307 |
308 | func Test_CreateInstantSettlement(t *testing.T) {
309 | createInstantSettlementPath := fmt.Sprintf(
310 | "/%s%s/ondemand",
311 | constants.VERSION_V1,
312 | constants.SETTLEMENT_URL,
313 | )
314 |
315 | // Successful response with all parameters
316 | successfulSettlementResp := map[string]interface{}{
317 | "id": "setlod_FNj7g2YS5J67Rz",
318 | "entity": "settlement.ondemand",
319 | "amount_requested": float64(200000),
320 | "amount_settled": float64(0),
321 | "amount_pending": float64(199410),
322 | "amount_reversed": float64(0),
323 | "fees": float64(590),
324 | "tax": float64(90),
325 | "currency": "INR",
326 | "settle_full_balance": false,
327 | "status": "initiated",
328 | "description": "Need this to make vendor payments.",
329 | "notes": map[string]interface{}{
330 | "notes_key_1": "Tea, Earl Grey, Hot",
331 | "notes_key_2": "Tea, Earl Grey… decaf.",
332 | },
333 | "created_at": float64(1596771429),
334 | }
335 |
336 | // Error response for insufficient amount
337 | insufficientAmountResp := map[string]interface{}{
338 | "error": map[string]interface{}{
339 | "code": "BAD_REQUEST_ERROR",
340 | "description": "Minimum amount that can be settled is ₹ 1.",
341 | },
342 | }
343 |
344 | tests := []RazorpayToolTestCase{
345 | {
346 | Name: "successful settlement creation with all parameters",
347 | Request: map[string]interface{}{
348 | "amount": float64(200000),
349 | "settle_full_balance": false,
350 | "description": "Need this to make vendor payments.",
351 | "notes": map[string]interface{}{
352 | "notes_key_1": "Tea, Earl Grey, Hot",
353 | "notes_key_2": "Tea, Earl Grey… decaf.",
354 | },
355 | },
356 | MockHttpClient: func() (*http.Client, *httptest.Server) {
357 | return mock.NewHTTPClient(
358 | mock.Endpoint{
359 | Path: createInstantSettlementPath,
360 | Method: "POST",
361 | Response: successfulSettlementResp,
362 | },
363 | )
364 | },
365 | ExpectError: false,
366 | ExpectedResult: successfulSettlementResp,
367 | },
368 | {
369 | Name: "settlement creation with required parameters only",
370 | Request: map[string]interface{}{
371 | "amount": float64(200000),
372 | },
373 | MockHttpClient: func() (*http.Client, *httptest.Server) {
374 | return mock.NewHTTPClient(
375 | mock.Endpoint{
376 | Path: createInstantSettlementPath,
377 | Method: "POST",
378 | Response: successfulSettlementResp,
379 | },
380 | )
381 | },
382 | ExpectError: false,
383 | ExpectedResult: successfulSettlementResp,
384 | },
385 | {
386 | Name: "settlement creation with insufficient amount",
387 | Request: map[string]interface{}{
388 | "amount": float64(10), // Less than minimum
389 | },
390 | MockHttpClient: func() (*http.Client, *httptest.Server) {
391 | return mock.NewHTTPClient(
392 | mock.Endpoint{
393 | Path: createInstantSettlementPath,
394 | Method: "POST",
395 | Response: insufficientAmountResp,
396 | },
397 | )
398 | },
399 | ExpectError: true,
400 | ExpectedErrMsg: "creating instant settlement failed: Minimum amount that " +
401 | "can be settled is ₹ 1.",
402 | },
403 | {
404 | Name: "missing amount parameter",
405 | Request: map[string]interface{}{},
406 | MockHttpClient: nil, // No HTTP client needed for validation error
407 | ExpectError: true,
408 | ExpectedErrMsg: "missing required parameter: amount",
409 | },
410 | }
411 |
412 | for _, tc := range tests {
413 | t.Run(tc.Name, func(t *testing.T) {
414 | runToolTest(t, tc, CreateInstantSettlement, "Instant Settlement")
415 | })
416 | }
417 | }
418 |
419 | func Test_FetchAllInstantSettlements(t *testing.T) {
420 | fetchAllInstantSettlementsPath := fmt.Sprintf(
421 | "/%s%s/ondemand",
422 | constants.VERSION_V1,
423 | constants.SETTLEMENT_URL,
424 | )
425 |
426 | // Sample response for successful fetch without expanded payouts
427 | basicSettlementListResp := map[string]interface{}{
428 | "entity": "collection",
429 | "count": float64(2),
430 | "items": []interface{}{
431 | map[string]interface{}{
432 | "id": "setlod_FNj7g2YS5J67Rz",
433 | "entity": "settlement.ondemand",
434 | "amount_requested": float64(200000),
435 | "amount_settled": float64(199410),
436 | "amount_pending": float64(0),
437 | "amount_reversed": float64(0),
438 | "fees": float64(590),
439 | "tax": float64(90),
440 | "currency": "INR",
441 | "settle_full_balance": false,
442 | "status": "processed",
443 | "description": "Need this to make vendor payments.",
444 | "notes": map[string]interface{}{
445 | "notes_key_1": "Tea, Earl Grey, Hot",
446 | "notes_key_2": "Tea, Earl Grey… decaf.",
447 | },
448 | "created_at": float64(1596771429),
449 | },
450 | map[string]interface{}{
451 | "id": "setlod_FJOp0jOWlalIvt",
452 | "entity": "settlement.ondemand",
453 | "amount_requested": float64(300000),
454 | "amount_settled": float64(299114),
455 | "amount_pending": float64(0),
456 | "amount_reversed": float64(0),
457 | "fees": float64(886),
458 | "tax": float64(136),
459 | "currency": "INR",
460 | "settle_full_balance": false,
461 | "status": "processed",
462 | "description": "Need this to buy stock.",
463 | "notes": map[string]interface{}{
464 | "notes_key_1": "Tea, Earl Grey, Hot",
465 | "notes_key_2": "Tea, Earl Grey… decaf.",
466 | },
467 | "created_at": float64(1595826576),
468 | },
469 | },
470 | }
471 |
472 | // Sample response with expanded payouts
473 | expandedSettlementListResp := map[string]interface{}{
474 | "entity": "collection",
475 | "count": float64(2),
476 | "items": []interface{}{
477 | map[string]interface{}{
478 | "id": "setlod_FNj7g2YS5J67Rz",
479 | "entity": "settlement.ondemand",
480 | "amount_requested": float64(200000),
481 | "amount_settled": float64(199410),
482 | "amount_pending": float64(0),
483 | "amount_reversed": float64(0),
484 | "fees": float64(590),
485 | "tax": float64(90),
486 | "currency": "INR",
487 | "settle_full_balance": false,
488 | "status": "processed",
489 | "description": "Need this to make vendor payments.",
490 | "notes": map[string]interface{}{
491 | "notes_key_1": "Tea, Earl Grey, Hot",
492 | "notes_key_2": "Tea, Earl Grey… decaf.",
493 | },
494 | "created_at": float64(1596771429),
495 | "ondemand_payouts": []interface{}{
496 | map[string]interface{}{
497 | "id": "pout_FNj7g2YS5J67Rz",
498 | "entity": "payout",
499 | "amount": float64(199410),
500 | "status": "processed",
501 | },
502 | },
503 | },
504 | map[string]interface{}{
505 | "id": "setlod_FJOp0jOWlalIvt",
506 | "entity": "settlement.ondemand",
507 | "amount_requested": float64(300000),
508 | "amount_settled": float64(299114),
509 | "amount_pending": float64(0),
510 | "amount_reversed": float64(0),
511 | "fees": float64(886),
512 | "tax": float64(136),
513 | "currency": "INR",
514 | "settle_full_balance": false,
515 | "status": "processed",
516 | "description": "Need this to buy stock.",
517 | "notes": map[string]interface{}{
518 | "notes_key_1": "Tea, Earl Grey, Hot",
519 | "notes_key_2": "Tea, Earl Grey… decaf.",
520 | },
521 | "created_at": float64(1595826576),
522 | "ondemand_payouts": []interface{}{
523 | map[string]interface{}{
524 | "id": "pout_FJOp0jOWlalIvt",
525 | "entity": "payout",
526 | "amount": float64(299114),
527 | "status": "processed",
528 | },
529 | },
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 instant settlements fetch with no parameters",
545 | Request: map[string]interface{}{},
546 | MockHttpClient: func() (*http.Client, *httptest.Server) {
547 | return mock.NewHTTPClient(
548 | mock.Endpoint{
549 | Path: fetchAllInstantSettlementsPath,
550 | Method: "GET",
551 | Response: basicSettlementListResp,
552 | },
553 | )
554 | },
555 | ExpectError: false,
556 | ExpectedResult: basicSettlementListResp,
557 | },
558 | {
559 | Name: "instant settlements fetch with pagination",
560 | Request: map[string]interface{}{
561 | "count": float64(10),
562 | "skip": float64(0),
563 | },
564 | MockHttpClient: func() (*http.Client, *httptest.Server) {
565 | return mock.NewHTTPClient(
566 | mock.Endpoint{
567 | Path: fetchAllInstantSettlementsPath,
568 | Method: "GET",
569 | Response: basicSettlementListResp,
570 | },
571 | )
572 | },
573 | ExpectError: false,
574 | ExpectedResult: basicSettlementListResp,
575 | },
576 | {
577 | Name: "instant settlements fetch with expanded payouts",
578 | Request: map[string]interface{}{
579 | "expand": []interface{}{"ondemand_payouts"},
580 | },
581 | MockHttpClient: func() (*http.Client, *httptest.Server) {
582 | return mock.NewHTTPClient(
583 | mock.Endpoint{
584 | Path: fetchAllInstantSettlementsPath,
585 | Method: "GET",
586 | Response: expandedSettlementListResp,
587 | },
588 | )
589 | },
590 | ExpectError: false,
591 | ExpectedResult: expandedSettlementListResp,
592 | },
593 | {
594 | Name: "instant settlements fetch with date range",
595 | Request: map[string]interface{}{
596 | "from": float64(1609459200), // 2021-01-01
597 | "to": float64(1640995199), // 2021-12-31
598 | },
599 | MockHttpClient: func() (*http.Client, *httptest.Server) {
600 | return mock.NewHTTPClient(
601 | mock.Endpoint{
602 | Path: fetchAllInstantSettlementsPath,
603 | Method: "GET",
604 | Response: basicSettlementListResp,
605 | },
606 | )
607 | },
608 | ExpectError: false,
609 | ExpectedResult: basicSettlementListResp,
610 | },
611 | {
612 | Name: "instant settlements fetch with invalid timestamp",
613 | Request: map[string]interface{}{
614 | "from": float64(900000000), // Invalid timestamp (too early)
615 | "to": float64(1600000000),
616 | },
617 | MockHttpClient: func() (*http.Client, *httptest.Server) {
618 | return mock.NewHTTPClient(
619 | mock.Endpoint{
620 | Path: fetchAllInstantSettlementsPath,
621 | Method: "GET",
622 | Response: invalidParamsResp,
623 | },
624 | )
625 | },
626 | ExpectError: true,
627 | ExpectedErrMsg: "fetching instant settlements failed: from must be " +
628 | "between 946684800 and 4765046400",
629 | },
630 | }
631 |
632 | for _, tc := range tests {
633 | t.Run(tc.Name, func(t *testing.T) {
634 | runToolTest(t, tc, FetchAllInstantSettlements, "Instant Settlements List")
635 | })
636 | }
637 | }
638 |
639 | func Test_FetchInstantSettlement(t *testing.T) {
640 | fetchInstantSettlementPathFmt := fmt.Sprintf(
641 | "/%s%s/ondemand/%%s",
642 | constants.VERSION_V1,
643 | constants.SETTLEMENT_URL,
644 | )
645 |
646 | instantSettlementResp := map[string]interface{}{
647 | "id": "setlod_FNj7g2YS5J67Rz",
648 | "entity": "settlement.ondemand",
649 | "amount_requested": float64(200000),
650 | "amount_settled": float64(199410),
651 | "amount_pending": float64(0),
652 | "amount_reversed": float64(0),
653 | "fees": float64(590),
654 | "tax": float64(90),
655 | "currency": "INR",
656 | "settle_full_balance": false,
657 | "status": "processed",
658 | "description": "Need this to make vendor payments.",
659 | "notes": map[string]interface{}{
660 | "notes_key_1": "Tea, Earl Grey, Hot",
661 | "notes_key_2": "Tea, Earl Grey… decaf.",
662 | },
663 | "created_at": float64(1596771429),
664 | }
665 |
666 | instantSettlementNotFoundResp := map[string]interface{}{
667 | "error": map[string]interface{}{
668 | "code": "BAD_REQUEST_ERROR",
669 | "description": "instant settlement not found",
670 | },
671 | }
672 |
673 | tests := []RazorpayToolTestCase{
674 | {
675 | Name: "successful instant settlement fetch",
676 | Request: map[string]interface{}{
677 | "settlement_id": "setlod_FNj7g2YS5J67Rz",
678 | },
679 | MockHttpClient: func() (*http.Client, *httptest.Server) {
680 | return mock.NewHTTPClient(
681 | mock.Endpoint{
682 | Path: fmt.Sprintf(fetchInstantSettlementPathFmt,
683 | "setlod_FNj7g2YS5J67Rz"),
684 | Method: "GET",
685 | Response: instantSettlementResp,
686 | },
687 | )
688 | },
689 | ExpectError: false,
690 | ExpectedResult: instantSettlementResp,
691 | },
692 | {
693 | Name: "instant settlement not found",
694 | Request: map[string]interface{}{
695 | "settlement_id": "setlod_invalid",
696 | },
697 | MockHttpClient: func() (*http.Client, *httptest.Server) {
698 | return mock.NewHTTPClient(
699 | mock.Endpoint{
700 | Path: fmt.Sprintf(fetchInstantSettlementPathFmt, "setlod_invalid"),
701 | Method: "GET",
702 | Response: instantSettlementNotFoundResp,
703 | },
704 | )
705 | },
706 | ExpectError: true,
707 | ExpectedErrMsg: "fetching instant settlement failed: " +
708 | "instant settlement not found",
709 | },
710 | {
711 | Name: "missing settlement_id parameter",
712 | Request: map[string]interface{}{},
713 | MockHttpClient: nil, // No HTTP client needed for validation error
714 | ExpectError: true,
715 | ExpectedErrMsg: "missing required parameter: settlement_id",
716 | },
717 | }
718 |
719 | for _, tc := range tests {
720 | t.Run(tc.Name, func(t *testing.T) {
721 | runToolTest(t, tc, FetchInstantSettlement, "Instant Settlement")
722 | })
723 | }
724 | }
725 |
```
--------------------------------------------------------------------------------
/pkg/mcpgo/tool_test.go:
--------------------------------------------------------------------------------
```go
1 | package mcpgo
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "testing"
7 |
8 | "github.com/mark3labs/mcp-go/mcp"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestNewTool(t *testing.T) {
13 | t.Run("creates tool with all fields", func(t *testing.T) {
14 | handler := func(
15 | ctx context.Context, req CallToolRequest) (*ToolResult, error) {
16 | return NewToolResultText("success"), nil
17 | }
18 | tool := NewTool(
19 | "test-tool",
20 | "Test description",
21 | []ToolParameter{WithString("param1")},
22 | handler,
23 | )
24 | assert.NotNil(t, tool)
25 | assert.NotNil(t, tool.GetHandler())
26 | })
27 |
28 | t.Run("creates tool with empty parameters", func(t *testing.T) {
29 | handler := func(
30 | ctx context.Context, req CallToolRequest) (*ToolResult, error) {
31 | return NewToolResultText("success"), nil
32 | }
33 | tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
34 | assert.NotNil(t, tool)
35 | })
36 | }
37 |
38 | func TestMark3labsToolImpl_GetHandler(t *testing.T) {
39 | t.Run("returns handler", func(t *testing.T) {
40 | handler := func(
41 | ctx context.Context, req CallToolRequest) (*ToolResult, error) {
42 | return NewToolResultText("success"), nil
43 | }
44 | tool := NewTool("test-tool", "Test", []ToolParameter{}, handler)
45 | assert.NotNil(t, tool.GetHandler())
46 | })
47 | }
48 |
49 | func TestMark3labsToolImpl_ToMCPServerTool(t *testing.T) {
50 | t.Run("converts tool with string parameter", func(t *testing.T) {
51 | tool := NewTool(
52 | "test-tool",
53 | "Test",
54 | []ToolParameter{WithString("param1")},
55 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
56 | return NewToolResultText("success"), nil
57 | },
58 | )
59 | mcpTool := tool.toMCPServerTool()
60 | assert.NotNil(t, mcpTool.Tool)
61 | assert.NotNil(t, mcpTool.Handler)
62 | })
63 |
64 | t.Run("converts tool with number parameter", func(t *testing.T) {
65 | tool := NewTool(
66 | "test-tool",
67 | "Test",
68 | []ToolParameter{WithNumber("param1")},
69 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
70 | return NewToolResultText("success"), nil
71 | },
72 | )
73 | mcpTool := tool.toMCPServerTool()
74 | assert.NotNil(t, mcpTool.Tool)
75 | })
76 |
77 | t.Run("converts tool with boolean parameter", func(t *testing.T) {
78 | tool := NewTool(
79 | "test-tool",
80 | "Test",
81 | []ToolParameter{WithBoolean("param1")},
82 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
83 | return NewToolResultText("success"), nil
84 | },
85 | )
86 | mcpTool := tool.toMCPServerTool()
87 | assert.NotNil(t, mcpTool.Tool)
88 | })
89 |
90 | t.Run("converts tool with object parameter", func(t *testing.T) {
91 | tool := NewTool(
92 | "test-tool",
93 | "Test",
94 | []ToolParameter{WithObject("param1")},
95 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
96 | return NewToolResultText("success"), nil
97 | },
98 | )
99 | mcpTool := tool.toMCPServerTool()
100 | assert.NotNil(t, mcpTool.Tool)
101 | })
102 |
103 | t.Run("converts tool with array parameter", func(t *testing.T) {
104 | tool := NewTool(
105 | "test-tool",
106 | "Test",
107 | []ToolParameter{WithArray("param1")},
108 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
109 | return NewToolResultText("success"), nil
110 | },
111 | )
112 | mcpTool := tool.toMCPServerTool()
113 | assert.NotNil(t, mcpTool.Tool)
114 | })
115 |
116 | t.Run("converts tool with integer parameter", func(t *testing.T) {
117 | param := ToolParameter{
118 | Name: "param1",
119 | Schema: map[string]interface{}{"type": "integer"},
120 | }
121 | tool := NewTool(
122 | "test-tool",
123 | "Test",
124 | []ToolParameter{param},
125 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
126 | return NewToolResultText("success"), nil
127 | },
128 | )
129 | mcpTool := tool.toMCPServerTool()
130 | assert.NotNil(t, mcpTool.Tool)
131 | })
132 |
133 | t.Run("converts tool with unknown type parameter", func(t *testing.T) {
134 | param := ToolParameter{
135 | Name: "param1",
136 | Schema: map[string]interface{}{"type": "unknown"},
137 | }
138 | tool := NewTool(
139 | "test-tool",
140 | "Test",
141 | []ToolParameter{param},
142 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
143 | return NewToolResultText("success"), nil
144 | },
145 | )
146 | mcpTool := tool.toMCPServerTool()
147 | assert.NotNil(t, mcpTool.Tool)
148 | })
149 |
150 | t.Run("converts tool with missing type parameter", func(t *testing.T) {
151 | param := ToolParameter{
152 | Name: "param1",
153 | Schema: map[string]interface{}{},
154 | }
155 | tool := NewTool(
156 | "test-tool",
157 | "Test",
158 | []ToolParameter{param},
159 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
160 | return NewToolResultText("success"), nil
161 | },
162 | )
163 | mcpTool := tool.toMCPServerTool()
164 | assert.NotNil(t, mcpTool.Tool)
165 | })
166 |
167 | t.Run("converts tool with non-string type", func(t *testing.T) {
168 | param := ToolParameter{
169 | Name: "param1",
170 | Schema: map[string]interface{}{"type": 123},
171 | }
172 | tool := NewTool(
173 | "test-tool",
174 | "Test",
175 | []ToolParameter{param},
176 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
177 | return NewToolResultText("success"), nil
178 | },
179 | )
180 | mcpTool := tool.toMCPServerTool()
181 | assert.NotNil(t, mcpTool.Tool)
182 | })
183 |
184 | t.Run("handler returns error result", func(t *testing.T) {
185 | tool := NewTool(
186 | "test-tool",
187 | "Test",
188 | []ToolParameter{},
189 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
190 | return NewToolResultError("error occurred"), nil
191 | },
192 | )
193 | mcpTool := tool.toMCPServerTool()
194 | assert.NotNil(t, mcpTool.Handler)
195 |
196 | req := mcp.CallToolRequest{
197 | Params: mcp.CallToolParams{
198 | Name: "test-tool",
199 | Arguments: map[string]interface{}{},
200 | },
201 | }
202 | result, err := mcpTool.Handler(context.Background(), req)
203 | assert.NoError(t, err)
204 | assert.NotNil(t, result)
205 | })
206 |
207 | t.Run("handler returns handler error", func(t *testing.T) {
208 | tool := NewTool(
209 | "test-tool",
210 | "Test",
211 | []ToolParameter{},
212 | func(ctx context.Context, req CallToolRequest) (*ToolResult, error) {
213 | return nil, assert.AnError
214 | },
215 | )
216 | mcpTool := tool.toMCPServerTool()
217 | assert.NotNil(t, mcpTool.Handler)
218 |
219 | req := mcp.CallToolRequest{
220 | Params: mcp.CallToolParams{
221 | Name: "test-tool",
222 | Arguments: map[string]interface{}{},
223 | },
224 | }
225 | result, err := mcpTool.Handler(context.Background(), req)
226 | assert.Error(t, err)
227 | assert.Nil(t, result)
228 | })
229 | }
230 |
231 | func TestPropertyOption_Min(t *testing.T) {
232 | t.Run("sets minimum for number", func(t *testing.T) {
233 | schema := map[string]interface{}{"type": "number"}
234 | Min(10.0)(schema)
235 | assert.Equal(t, 10.0, schema["minimum"])
236 | })
237 |
238 | t.Run("sets minimum for integer", func(t *testing.T) {
239 | schema := map[string]interface{}{"type": "integer"}
240 | Min(5.0)(schema)
241 | assert.Equal(t, 5.0, schema["minimum"])
242 | })
243 |
244 | t.Run("sets minLength for string", func(t *testing.T) {
245 | schema := map[string]interface{}{"type": "string"}
246 | Min(3.0)(schema)
247 | assert.Equal(t, 3, schema["minLength"])
248 | })
249 |
250 | t.Run("sets minItems for array", func(t *testing.T) {
251 | schema := map[string]interface{}{"type": "array"}
252 | Min(2.0)(schema)
253 | assert.Equal(t, 2, schema["minItems"])
254 | })
255 |
256 | t.Run("ignores for unknown type", func(t *testing.T) {
257 | schema := map[string]interface{}{"type": "boolean"}
258 | Min(1.0)(schema)
259 | assert.NotContains(t, schema, "minimum")
260 | assert.NotContains(t, schema, "minLength")
261 | assert.NotContains(t, schema, "minItems")
262 | })
263 |
264 | t.Run("ignores for missing type", func(t *testing.T) {
265 | schema := map[string]interface{}{}
266 | Min(1.0)(schema)
267 | assert.NotContains(t, schema, "minimum")
268 | })
269 |
270 | t.Run("ignores for non-string type", func(t *testing.T) {
271 | schema := map[string]interface{}{"type": 123}
272 | Min(1.0)(schema)
273 | assert.NotContains(t, schema, "minimum")
274 | })
275 | }
276 |
277 | func TestPropertyOption_Max(t *testing.T) {
278 | t.Run("sets maximum for number", func(t *testing.T) {
279 | schema := map[string]interface{}{"type": "number"}
280 | Max(100.0)(schema)
281 | assert.Equal(t, 100.0, schema["maximum"])
282 | })
283 |
284 | t.Run("sets maximum for integer", func(t *testing.T) {
285 | schema := map[string]interface{}{"type": "integer"}
286 | Max(50.0)(schema)
287 | assert.Equal(t, 50.0, schema["maximum"])
288 | })
289 |
290 | t.Run("sets maxLength for string", func(t *testing.T) {
291 | schema := map[string]interface{}{"type": "string"}
292 | Max(10.0)(schema)
293 | assert.Equal(t, 10, schema["maxLength"])
294 | })
295 |
296 | t.Run("sets maxItems for array", func(t *testing.T) {
297 | schema := map[string]interface{}{"type": "array"}
298 | Max(5.0)(schema)
299 | assert.Equal(t, 5, schema["maxItems"])
300 | })
301 |
302 | t.Run("ignores for unknown type", func(t *testing.T) {
303 | schema := map[string]interface{}{"type": "boolean"}
304 | Max(1.0)(schema)
305 | assert.NotContains(t, schema, "maximum")
306 | })
307 |
308 | t.Run("ignores for missing type", func(t *testing.T) {
309 | schema := map[string]interface{}{}
310 | Max(1.0)(schema)
311 | assert.NotContains(t, schema, "maximum")
312 | })
313 |
314 | t.Run("ignores for non-string type value", func(t *testing.T) {
315 | schema := map[string]interface{}{"type": 123}
316 | Max(1.0)(schema)
317 | assert.NotContains(t, schema, "maximum")
318 | })
319 | }
320 |
321 | func TestPropertyOption_Pattern(t *testing.T) {
322 | t.Run("sets pattern for string", func(t *testing.T) {
323 | schema := map[string]interface{}{"type": "string"}
324 | Pattern("^[a-z]+$")(schema)
325 | assert.Equal(t, "^[a-z]+$", schema["pattern"])
326 | })
327 |
328 | t.Run("ignores for non-string type", func(t *testing.T) {
329 | schema := map[string]interface{}{"type": "number"}
330 | Pattern("^[a-z]+$")(schema)
331 | assert.NotContains(t, schema, "pattern")
332 | })
333 |
334 | t.Run("ignores for missing type", func(t *testing.T) {
335 | schema := map[string]interface{}{}
336 | Pattern("^[a-z]+$")(schema)
337 | assert.NotContains(t, schema, "pattern")
338 | })
339 |
340 | t.Run("ignores for non-string type value", func(t *testing.T) {
341 | schema := map[string]interface{}{"type": 123}
342 | Pattern("^[a-z]+$")(schema)
343 | assert.NotContains(t, schema, "pattern")
344 | })
345 | }
346 |
347 | func TestPropertyOption_Enum(t *testing.T) {
348 | t.Run("sets enum values", func(t *testing.T) {
349 | schema := map[string]interface{}{}
350 | Enum("value1", "value2", "value3")(schema)
351 | assert.Equal(t, []interface{}{"value1", "value2", "value3"}, schema["enum"])
352 | })
353 |
354 | t.Run("sets enum with mixed types", func(t *testing.T) {
355 | schema := map[string]interface{}{}
356 | Enum("value1", 123, true)(schema)
357 | assert.Equal(t, []interface{}{"value1", 123, true}, schema["enum"])
358 | })
359 | }
360 |
361 | func TestPropertyOption_DefaultValue(t *testing.T) {
362 | t.Run("sets default string value", func(t *testing.T) {
363 | schema := map[string]interface{}{}
364 | DefaultValue("default")(schema)
365 | assert.Equal(t, "default", schema["default"])
366 | })
367 |
368 | t.Run("sets default number value", func(t *testing.T) {
369 | schema := map[string]interface{}{}
370 | DefaultValue(42.0)(schema)
371 | assert.Equal(t, 42.0, schema["default"])
372 | })
373 |
374 | t.Run("sets default boolean value", func(t *testing.T) {
375 | schema := map[string]interface{}{}
376 | DefaultValue(true)(schema)
377 | assert.Equal(t, true, schema["default"])
378 | })
379 | }
380 |
381 | func TestPropertyOption_MaxProperties(t *testing.T) {
382 | t.Run("sets maxProperties for object", func(t *testing.T) {
383 | schema := map[string]interface{}{"type": "object"}
384 | MaxProperties(5)(schema)
385 | assert.Equal(t, 5, schema["maxProperties"])
386 | })
387 |
388 | t.Run("ignores for non-object type", func(t *testing.T) {
389 | schema := map[string]interface{}{"type": "string"}
390 | MaxProperties(5)(schema)
391 | assert.NotContains(t, schema, "maxProperties")
392 | })
393 |
394 | t.Run("ignores for missing type", func(t *testing.T) {
395 | schema := map[string]interface{}{}
396 | MaxProperties(5)(schema)
397 | assert.NotContains(t, schema, "maxProperties")
398 | })
399 | }
400 |
401 | func TestPropertyOption_MinProperties(t *testing.T) {
402 | t.Run("sets minProperties for object", func(t *testing.T) {
403 | schema := map[string]interface{}{"type": "object"}
404 | MinProperties(2)(schema)
405 | assert.Equal(t, 2, schema["minProperties"])
406 | })
407 |
408 | t.Run("ignores for non-object type", func(t *testing.T) {
409 | schema := map[string]interface{}{"type": "string"}
410 | MinProperties(2)(schema)
411 | assert.NotContains(t, schema, "minProperties")
412 | })
413 | }
414 |
415 | func TestPropertyOption_Required(t *testing.T) {
416 | t.Run("sets required flag", func(t *testing.T) {
417 | schema := map[string]interface{}{}
418 | Required()(schema)
419 | assert.Equal(t, true, schema["required"])
420 | })
421 | }
422 |
423 | func TestPropertyOption_Description(t *testing.T) {
424 | t.Run("sets description", func(t *testing.T) {
425 | schema := map[string]interface{}{}
426 | Description("Test description")(schema)
427 | assert.Equal(t, "Test description", schema["description"])
428 | })
429 | }
430 |
431 | func TestToolParameter_ApplyPropertyOptions(t *testing.T) {
432 | t.Run("applies single option", func(t *testing.T) {
433 | param := ToolParameter{
434 | Name: "test",
435 | Schema: map[string]interface{}{"type": "string"},
436 | }
437 | param.applyPropertyOptions(Description("Test desc"))
438 | assert.Equal(t, "Test desc", param.Schema["description"])
439 | })
440 |
441 | t.Run("applies multiple options", func(t *testing.T) {
442 | param := ToolParameter{
443 | Name: "test",
444 | Schema: map[string]interface{}{"type": "string"},
445 | }
446 | param.applyPropertyOptions(
447 | Description("Test desc"),
448 | Required(),
449 | Min(3.0),
450 | )
451 | assert.Equal(t, "Test desc", param.Schema["description"])
452 | assert.Equal(t, true, param.Schema["required"])
453 | assert.Equal(t, 3, param.Schema["minLength"])
454 | })
455 |
456 | t.Run("applies no options", func(t *testing.T) {
457 | param := ToolParameter{
458 | Name: "test",
459 | Schema: map[string]interface{}{"type": "string"},
460 | }
461 | param.applyPropertyOptions()
462 | assert.Equal(t, "string", param.Schema["type"])
463 | })
464 | }
465 |
466 | func TestWithString(t *testing.T) {
467 | t.Run("creates string parameter without options", func(t *testing.T) {
468 | param := WithString("test")
469 | assert.Equal(t, "test", param.Name)
470 | assert.Equal(t, "string", param.Schema["type"])
471 | })
472 |
473 | t.Run("creates string parameter with options", func(t *testing.T) {
474 | param := WithString("test", Description("Test"), Required(), Min(3.0))
475 | assert.Equal(t, "test", param.Name)
476 | assert.Equal(t, "string", param.Schema["type"])
477 | assert.Equal(t, "Test", param.Schema["description"])
478 | assert.Equal(t, true, param.Schema["required"])
479 | assert.Equal(t, 3, param.Schema["minLength"])
480 | })
481 | }
482 |
483 | func TestWithNumber(t *testing.T) {
484 | t.Run("creates number parameter without options", func(t *testing.T) {
485 | param := WithNumber("test")
486 | assert.Equal(t, "test", param.Name)
487 | assert.Equal(t, "number", param.Schema["type"])
488 | })
489 |
490 | t.Run("creates number parameter with options", func(t *testing.T) {
491 | param := WithNumber("test", Min(1.0), Max(100.0))
492 | assert.Equal(t, "test", param.Name)
493 | assert.Equal(t, "number", param.Schema["type"])
494 | assert.Equal(t, 1.0, param.Schema["minimum"])
495 | assert.Equal(t, 100.0, param.Schema["maximum"])
496 | })
497 | }
498 |
499 | func TestWithBoolean(t *testing.T) {
500 | t.Run("creates boolean parameter", func(t *testing.T) {
501 | param := WithBoolean("test")
502 | assert.Equal(t, "test", param.Name)
503 | assert.Equal(t, "boolean", param.Schema["type"])
504 | })
505 | }
506 |
507 | func TestWithObject(t *testing.T) {
508 | t.Run("creates object parameter", func(t *testing.T) {
509 | param := WithObject("test")
510 | assert.Equal(t, "test", param.Name)
511 | assert.Equal(t, "object", param.Schema["type"])
512 | })
513 |
514 | t.Run("creates object parameter with options", func(t *testing.T) {
515 | param := WithObject("test", MinProperties(1), MaxProperties(5))
516 | assert.Equal(t, "test", param.Name)
517 | assert.Equal(t, "object", param.Schema["type"])
518 | assert.Equal(t, 1, param.Schema["minProperties"])
519 | assert.Equal(t, 5, param.Schema["maxProperties"])
520 | })
521 | }
522 |
523 | func TestWithArray(t *testing.T) {
524 | t.Run("creates array parameter", func(t *testing.T) {
525 | param := WithArray("test")
526 | assert.Equal(t, "test", param.Name)
527 | assert.Equal(t, "array", param.Schema["type"])
528 | })
529 |
530 | t.Run("creates array parameter with options", func(t *testing.T) {
531 | param := WithArray("test", Min(1.0), Max(10.0))
532 | assert.Equal(t, "test", param.Name)
533 | assert.Equal(t, "array", param.Schema["type"])
534 | assert.Equal(t, 1, param.Schema["minItems"])
535 | assert.Equal(t, 10, param.Schema["maxItems"])
536 | })
537 | }
538 |
539 | func TestAddNumberPropertyOptions(t *testing.T) {
540 | t.Run("adds minimum", func(t *testing.T) {
541 | schema := map[string]interface{}{"minimum": 10.0}
542 | opts := addNumberPropertyOptions(nil, schema)
543 | assert.NotNil(t, opts)
544 | })
545 |
546 | t.Run("adds maximum", func(t *testing.T) {
547 | schema := map[string]interface{}{"maximum": 100.0}
548 | opts := addNumberPropertyOptions(nil, schema)
549 | assert.NotNil(t, opts)
550 | })
551 |
552 | t.Run("adds both minimum and maximum", func(t *testing.T) {
553 | schema := map[string]interface{}{
554 | "minimum": 10.0,
555 | "maximum": 100.0,
556 | }
557 | opts := addNumberPropertyOptions(nil, schema)
558 | assert.NotNil(t, opts)
559 | })
560 |
561 | t.Run("handles non-float64 minimum", func(t *testing.T) {
562 | schema := map[string]interface{}{"minimum": "not-a-number"}
563 | opts := addNumberPropertyOptions(nil, schema)
564 | assert.Nil(t, opts)
565 | })
566 | }
567 |
568 | func TestAddStringPropertyOptions(t *testing.T) {
569 | t.Run("adds minLength", func(t *testing.T) {
570 | schema := map[string]interface{}{"minLength": 3}
571 | opts := addStringPropertyOptions(nil, schema)
572 | assert.NotNil(t, opts)
573 | })
574 |
575 | t.Run("adds maxLength", func(t *testing.T) {
576 | schema := map[string]interface{}{"maxLength": 10}
577 | opts := addStringPropertyOptions(nil, schema)
578 | assert.NotNil(t, opts)
579 | })
580 |
581 | t.Run("adds pattern", func(t *testing.T) {
582 | schema := map[string]interface{}{"pattern": "^[a-z]+$"}
583 | opts := addStringPropertyOptions(nil, schema)
584 | assert.NotNil(t, opts)
585 | })
586 |
587 | t.Run("adds all string options", func(t *testing.T) {
588 | schema := map[string]interface{}{
589 | "minLength": 3,
590 | "maxLength": 10,
591 | "pattern": "^[a-z]+$",
592 | }
593 | opts := addStringPropertyOptions(nil, schema)
594 | assert.NotNil(t, opts)
595 | })
596 | }
597 |
598 | func TestAddDefaultValueOptions(t *testing.T) {
599 | t.Run("adds string default", func(t *testing.T) {
600 | opts := addDefaultValueOptions(nil, "default")
601 | assert.NotNil(t, opts)
602 | })
603 |
604 | t.Run("adds float64 default", func(t *testing.T) {
605 | opts := addDefaultValueOptions(nil, 42.0)
606 | assert.NotNil(t, opts)
607 | })
608 |
609 | t.Run("adds bool default", func(t *testing.T) {
610 | opts := addDefaultValueOptions(nil, true)
611 | assert.NotNil(t, opts)
612 | })
613 |
614 | t.Run("ignores unknown type", func(t *testing.T) {
615 | opts := addDefaultValueOptions(nil, []string{"test"})
616 | assert.Nil(t, opts)
617 | })
618 | }
619 |
620 | func TestAddEnumOptions(t *testing.T) {
621 | t.Run("adds enum with string values", func(t *testing.T) {
622 | enumValues := []interface{}{"value1", "value2", "value3"}
623 | opts := addEnumOptions(nil, enumValues)
624 | assert.NotNil(t, opts)
625 | })
626 |
627 | t.Run("adds enum with mixed values", func(t *testing.T) {
628 | enumValues := []interface{}{"value1", 123, "value2"}
629 | opts := addEnumOptions(nil, enumValues)
630 | assert.NotNil(t, opts)
631 | })
632 |
633 | t.Run("handles non-array enum", func(t *testing.T) {
634 | opts := addEnumOptions(nil, "not-an-array")
635 | assert.Nil(t, opts)
636 | })
637 |
638 | t.Run("handles empty enum array", func(t *testing.T) {
639 | enumValues := []interface{}{123, 456} // Non-string values
640 | opts := addEnumOptions(nil, enumValues)
641 | assert.Nil(t, opts) // Should return nil since no string values
642 | })
643 | }
644 |
645 | func TestAddObjectPropertyOptions(t *testing.T) {
646 | t.Run("adds maxProperties", func(t *testing.T) {
647 | schema := map[string]interface{}{"maxProperties": 5}
648 | opts := addObjectPropertyOptions(nil, schema)
649 | assert.NotNil(t, opts)
650 | })
651 |
652 | t.Run("adds minProperties", func(t *testing.T) {
653 | schema := map[string]interface{}{"minProperties": 2}
654 | opts := addObjectPropertyOptions(nil, schema)
655 | assert.NotNil(t, opts)
656 | })
657 |
658 | t.Run("adds both properties", func(t *testing.T) {
659 | schema := map[string]interface{}{
660 | "minProperties": 1,
661 | "maxProperties": 5,
662 | }
663 | opts := addObjectPropertyOptions(nil, schema)
664 | assert.NotNil(t, opts)
665 | })
666 | }
667 |
668 | func TestAddArrayPropertyOptions(t *testing.T) {
669 | t.Run("adds minItems", func(t *testing.T) {
670 | schema := map[string]interface{}{"minItems": 1}
671 | opts := addArrayPropertyOptions(nil, schema)
672 | assert.NotNil(t, opts)
673 | })
674 |
675 | t.Run("adds maxItems", func(t *testing.T) {
676 | schema := map[string]interface{}{"maxItems": 10}
677 | opts := addArrayPropertyOptions(nil, schema)
678 | assert.NotNil(t, opts)
679 | })
680 |
681 | t.Run("adds both items", func(t *testing.T) {
682 | schema := map[string]interface{}{
683 | "minItems": 1,
684 | "maxItems": 10,
685 | }
686 | opts := addArrayPropertyOptions(nil, schema)
687 | assert.NotNil(t, opts)
688 | })
689 | }
690 |
691 | func TestConvertSchemaToPropertyOptions(t *testing.T) {
692 | t.Run("converts complete schema", func(t *testing.T) {
693 | schema := map[string]interface{}{
694 | "type": "string",
695 | "description": "Test param",
696 | "required": true,
697 | "minLength": 3,
698 | "maxLength": 10,
699 | "pattern": "^[a-z]+$",
700 | "default": "default",
701 | }
702 | opts := convertSchemaToPropertyOptions(schema)
703 | assert.NotNil(t, opts)
704 | })
705 |
706 | t.Run("converts number schema", func(t *testing.T) {
707 | schema := map[string]interface{}{
708 | "type": "number",
709 | "minimum": 1.0,
710 | "maximum": 100.0,
711 | "default": 42.0,
712 | }
713 | opts := convertSchemaToPropertyOptions(schema)
714 | assert.NotNil(t, opts)
715 | })
716 |
717 | t.Run("converts object schema", func(t *testing.T) {
718 | schema := map[string]interface{}{
719 | "type": "object",
720 | "minProperties": 1,
721 | "maxProperties": 5,
722 | }
723 | opts := convertSchemaToPropertyOptions(schema)
724 | assert.NotNil(t, opts)
725 | })
726 |
727 | t.Run("converts array schema", func(t *testing.T) {
728 | schema := map[string]interface{}{
729 | "type": "array",
730 | "minItems": 1,
731 | "maxItems": 10,
732 | }
733 | opts := convertSchemaToPropertyOptions(schema)
734 | assert.NotNil(t, opts)
735 | })
736 |
737 | t.Run("converts schema with enum", func(t *testing.T) {
738 | schema := map[string]interface{}{
739 | "type": "string",
740 | "enum": []interface{}{"value1", "value2"},
741 | }
742 | opts := convertSchemaToPropertyOptions(schema)
743 | assert.NotNil(t, opts)
744 | })
745 |
746 | t.Run("handles empty description", func(t *testing.T) {
747 | schema := map[string]interface{}{
748 | "type": "string",
749 | "description": "",
750 | }
751 | opts := convertSchemaToPropertyOptions(schema)
752 | // Empty description should not be added
753 | // In Go, a nil slice is valid and has length 0
754 | assert.Len(t, opts, 0)
755 | })
756 |
757 | t.Run("handles false required", func(t *testing.T) {
758 | schema := map[string]interface{}{
759 | "type": "string",
760 | "required": false,
761 | }
762 | opts := convertSchemaToPropertyOptions(schema)
763 | // False required should not be added
764 | // In Go, a nil slice is valid and has length 0
765 | assert.Len(t, opts, 0)
766 | })
767 | }
768 |
769 | func TestNewToolResultJSON(t *testing.T) {
770 | t.Run("creates JSON result from map", func(t *testing.T) {
771 | data := map[string]interface{}{
772 | "key": "value",
773 | "num": 42,
774 | }
775 | result, err := NewToolResultJSON(data)
776 | assert.NoError(t, err)
777 | assert.NotNil(t, result)
778 | assert.False(t, result.IsError)
779 | assert.NotEmpty(t, result.Text)
780 |
781 | // Verify it's valid JSON
782 | var decoded map[string]interface{}
783 | err = json.Unmarshal([]byte(result.Text), &decoded)
784 | assert.NoError(t, err)
785 | assert.Equal(t, "value", decoded["key"])
786 | })
787 |
788 | t.Run("creates JSON result from struct", func(t *testing.T) {
789 | type TestStruct struct {
790 | Name string `json:"name"`
791 | Age int `json:"age"`
792 | }
793 | data := TestStruct{Name: "Test", Age: 30}
794 | result, err := NewToolResultJSON(data)
795 | assert.NoError(t, err)
796 | assert.NotNil(t, result)
797 | assert.False(t, result.IsError)
798 | })
799 |
800 | t.Run("creates JSON result from array", func(t *testing.T) {
801 | data := []string{"item1", "item2"}
802 | result, err := NewToolResultJSON(data)
803 | assert.NoError(t, err)
804 | assert.NotNil(t, result)
805 | })
806 |
807 | t.Run("handles unmarshalable data", func(t *testing.T) {
808 | // Create a channel which cannot be marshaled to JSON
809 | data := make(chan int)
810 | result, err := NewToolResultJSON(data)
811 | assert.Error(t, err)
812 | assert.Nil(t, result)
813 | })
814 | }
815 |
816 | func TestNewToolResultText(t *testing.T) {
817 | t.Run("creates text result", func(t *testing.T) {
818 | result := NewToolResultText("test text")
819 | assert.NotNil(t, result)
820 | assert.Equal(t, "test text", result.Text)
821 | assert.False(t, result.IsError)
822 | assert.Nil(t, result.Content)
823 | })
824 |
825 | t.Run("creates empty text result", func(t *testing.T) {
826 | result := NewToolResultText("")
827 | assert.NotNil(t, result)
828 | assert.Equal(t, "", result.Text)
829 | assert.False(t, result.IsError)
830 | })
831 | }
832 |
833 | func TestNewToolResultError(t *testing.T) {
834 | t.Run("creates error result", func(t *testing.T) {
835 | result := NewToolResultError("error message")
836 | assert.NotNil(t, result)
837 | assert.Equal(t, "error message", result.Text)
838 | assert.True(t, result.IsError)
839 | assert.Nil(t, result.Content)
840 | })
841 |
842 | t.Run("creates empty error result", func(t *testing.T) {
843 | result := NewToolResultError("")
844 | assert.NotNil(t, result)
845 | assert.Equal(t, "", result.Text)
846 | assert.True(t, result.IsError)
847 | })
848 | }
849 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/qr_codes_test.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 |
9 | "github.com/razorpay/razorpay-go/constants"
10 |
11 | "github.com/razorpay/razorpay-mcp-server/pkg/razorpay/mock"
12 | )
13 |
14 | func Test_CreateQRCode(t *testing.T) {
15 | createQRCodePath := fmt.Sprintf(
16 | "/%s%s",
17 | constants.VERSION_V1,
18 | constants.QRCODE_URL,
19 | )
20 |
21 | qrCodeWithAllParamsResp := map[string]interface{}{
22 | "id": "qr_HMsVL8HOpbMcjU",
23 | "entity": "qr_code",
24 | "created_at": float64(1623660301),
25 | "name": "Store Front Display",
26 | "usage": "single_use",
27 | "type": "upi_qr",
28 | "image_url": "https://rzp.io/i/BWcUVrLp",
29 | "payment_amount": float64(300),
30 | "status": "active",
31 | "description": "For Store 1",
32 | "fixed_amount": true,
33 | "payments_amount_received": float64(0),
34 | "payments_count_received": float64(0),
35 | "notes": map[string]interface{}{
36 | "purpose": "Test UPI QR Code notes",
37 | },
38 | "customer_id": "cust_HKsR5se84c5LTO",
39 | "close_by": float64(1681615838),
40 | }
41 |
42 | qrCodeWithRequiredParamsResp := map[string]interface{}{
43 | "id": "qr_HMsVL8HOpbMcjU",
44 | "entity": "qr_code",
45 | "created_at": float64(1623660301),
46 | "usage": "multiple_use",
47 | "type": "upi_qr",
48 | "image_url": "https://rzp.io/i/BWcUVrLp",
49 | "status": "active",
50 | "fixed_amount": false,
51 | "payments_amount_received": float64(0),
52 | "payments_count_received": float64(0),
53 | }
54 |
55 | qrCodeWithoutPaymentAmountResp := map[string]interface{}{
56 | "id": "qr_HMsVL8HOpbMcjU",
57 | "entity": "qr_code",
58 | "created_at": float64(1623660301),
59 | "name": "Store Front Display",
60 | "usage": "single_use",
61 | "type": "upi_qr",
62 | "image_url": "https://rzp.io/i/BWcUVrLp",
63 | "status": "active",
64 | "description": "For Store 1",
65 | "fixed_amount": false,
66 | "payments_amount_received": float64(0),
67 | "payments_count_received": float64(0),
68 | }
69 |
70 | errorResp := map[string]interface{}{
71 | "error": map[string]interface{}{
72 | "code": "BAD_REQUEST_ERROR",
73 | "description": "The type field is invalid",
74 | },
75 | }
76 |
77 | tests := []RazorpayToolTestCase{
78 | {
79 | Name: "successful QR code creation with all parameters",
80 | Request: map[string]interface{}{
81 | "type": "upi_qr",
82 | "name": "Store Front Display",
83 | "usage": "single_use",
84 | "fixed_amount": true,
85 | "payment_amount": float64(300),
86 | "description": "For Store 1",
87 | "customer_id": "cust_HKsR5se84c5LTO",
88 | "close_by": float64(1681615838),
89 | "notes": map[string]interface{}{
90 | "purpose": "Test UPI QR Code notes",
91 | },
92 | },
93 | MockHttpClient: func() (*http.Client, *httptest.Server) {
94 | return mock.NewHTTPClient(
95 | mock.Endpoint{
96 | Path: createQRCodePath,
97 | Method: "POST",
98 | Response: qrCodeWithAllParamsResp,
99 | },
100 | )
101 | },
102 | ExpectError: false,
103 | ExpectedResult: qrCodeWithAllParamsResp,
104 | },
105 | {
106 | Name: "successful QR code creation with required params only",
107 | Request: map[string]interface{}{
108 | "type": "upi_qr",
109 | "usage": "multiple_use",
110 | },
111 | MockHttpClient: func() (*http.Client, *httptest.Server) {
112 | return mock.NewHTTPClient(
113 | mock.Endpoint{
114 | Path: createQRCodePath,
115 | Method: "POST",
116 | Response: qrCodeWithRequiredParamsResp,
117 | },
118 | )
119 | },
120 | ExpectError: false,
121 | ExpectedResult: qrCodeWithRequiredParamsResp,
122 | },
123 | {
124 | Name: "successful QR code creation without payment amount",
125 | Request: map[string]interface{}{
126 | "type": "upi_qr",
127 | "name": "Store Front Display",
128 | "usage": "single_use",
129 | "fixed_amount": false,
130 | "description": "For Store 1",
131 | },
132 | MockHttpClient: func() (*http.Client, *httptest.Server) {
133 | return mock.NewHTTPClient(
134 | mock.Endpoint{
135 | Path: createQRCodePath,
136 | Method: "POST",
137 | Response: qrCodeWithoutPaymentAmountResp,
138 | },
139 | )
140 | },
141 | ExpectError: false,
142 | ExpectedResult: qrCodeWithoutPaymentAmountResp,
143 | },
144 | {
145 | Name: "missing required type parameter",
146 | Request: map[string]interface{}{
147 | "usage": "single_use",
148 | },
149 | MockHttpClient: nil,
150 | ExpectError: true,
151 | ExpectedErrMsg: "missing required parameter: type",
152 | },
153 | {
154 | Name: "missing required usage parameter",
155 | Request: map[string]interface{}{
156 | "type": "upi_qr",
157 | },
158 | MockHttpClient: nil,
159 | ExpectError: true,
160 | ExpectedErrMsg: "missing required parameter: usage",
161 | },
162 | {
163 | Name: "validator error - invalid parameter type",
164 | Request: map[string]interface{}{
165 | "type": 123,
166 | "usage": "single_use",
167 | },
168 | MockHttpClient: nil,
169 | ExpectError: true,
170 | ExpectedErrMsg: "Validation errors",
171 | },
172 | {
173 | Name: "fixed_amount true but payment_amount missing",
174 | Request: map[string]interface{}{
175 | "type": "upi_qr",
176 | "usage": "single_use",
177 | "fixed_amount": true,
178 | },
179 | MockHttpClient: nil,
180 | ExpectError: true,
181 | ExpectedErrMsg: "payment_amount is required when fixed_amount is true",
182 | },
183 | {
184 | Name: "invalid type parameter",
185 | Request: map[string]interface{}{
186 | "type": "invalid_type",
187 | "usage": "single_use",
188 | },
189 | MockHttpClient: func() (*http.Client, *httptest.Server) {
190 | return mock.NewHTTPClient(
191 | mock.Endpoint{
192 | Path: createQRCodePath,
193 | Method: "POST",
194 | Response: errorResp,
195 | },
196 | )
197 | },
198 | ExpectError: true,
199 | ExpectedErrMsg: "creating QR code failed: The type field is invalid",
200 | },
201 | }
202 |
203 | for _, tc := range tests {
204 | t.Run(tc.Name, func(t *testing.T) {
205 | runToolTest(t, tc, CreateQRCode, "QR Code")
206 | })
207 | }
208 | }
209 |
210 | func Test_FetchAllQRCodes(t *testing.T) {
211 | qrCodesPath := fmt.Sprintf(
212 | "/%s%s",
213 | constants.VERSION_V1,
214 | constants.QRCODE_URL,
215 | )
216 |
217 | allQRCodesResp := map[string]interface{}{
218 | "entity": "collection",
219 | "count": float64(2),
220 | "items": []interface{}{
221 | map[string]interface{}{
222 | "id": "qr_HO2jGkWReVBMNu",
223 | "entity": "qr_code",
224 | "created_at": float64(1623914648),
225 | "name": "Store_1",
226 | "usage": "single_use",
227 | "type": "upi_qr",
228 | "image_url": "https://rzp.io/i/w2CEwYmkAu",
229 | "payment_amount": float64(300),
230 | "status": "active",
231 | "description": "For Store 1",
232 | "fixed_amount": true,
233 | "payments_amount_received": float64(0),
234 | "payments_count_received": float64(0),
235 | "notes": map[string]interface{}{
236 | "purpose": "Test UPI QR Code notes",
237 | },
238 | "customer_id": "cust_HKsR5se84c5LTO",
239 | "close_by": float64(1681615838),
240 | "closed_at": nil,
241 | "close_reason": nil,
242 | },
243 | map[string]interface{}{
244 | "id": "qr_HO2e0813YlchUn",
245 | "entity": "qr_code",
246 | "created_at": float64(1623914349),
247 | "name": "Acme Groceries",
248 | "usage": "multiple_use",
249 | "type": "upi_qr",
250 | "image_url": "https://rzp.io/i/X6QM7LL",
251 | "payment_amount": nil,
252 | "status": "closed",
253 | "description": "Buy fresh groceries",
254 | "fixed_amount": false,
255 | "payments_amount_received": float64(200),
256 | "payments_count_received": float64(1),
257 | "notes": map[string]interface{}{
258 | "Branch": "Bangalore - Rajaji Nagar",
259 | },
260 | "customer_id": "cust_HKsR5se84c5LTO",
261 | "close_by": float64(1625077799),
262 | "closed_at": float64(1623914515),
263 | "close_reason": "on_demand",
264 | },
265 | },
266 | }
267 |
268 | errorResp := map[string]interface{}{
269 | "error": map[string]interface{}{
270 | "code": "BAD_REQUEST_ERROR",
271 | "description": "The query parameters are invalid",
272 | },
273 | }
274 |
275 | tests := []RazorpayToolTestCase{
276 | {
277 | Name: "successful fetch all QR codes with no parameters",
278 | Request: map[string]interface{}{},
279 | MockHttpClient: func() (*http.Client, *httptest.Server) {
280 | return mock.NewHTTPClient(
281 | mock.Endpoint{
282 | Path: qrCodesPath,
283 | Method: "GET",
284 | Response: allQRCodesResp,
285 | },
286 | )
287 | },
288 | ExpectError: false,
289 | ExpectedResult: allQRCodesResp,
290 | },
291 | {
292 | Name: "successful fetch all QR codes with count parameter",
293 | Request: map[string]interface{}{
294 | "count": float64(2),
295 | },
296 | MockHttpClient: func() (*http.Client, *httptest.Server) {
297 | return mock.NewHTTPClient(
298 | mock.Endpoint{
299 | Path: qrCodesPath,
300 | Method: "GET",
301 | Response: allQRCodesResp,
302 | },
303 | )
304 | },
305 | ExpectError: false,
306 | ExpectedResult: allQRCodesResp,
307 | },
308 | {
309 | Name: "successful fetch all QR codes with pagination parameters",
310 | Request: map[string]interface{}{
311 | "from": float64(1622000000),
312 | "to": float64(1625000000),
313 | "count": float64(2),
314 | "skip": float64(0),
315 | },
316 | MockHttpClient: func() (*http.Client, *httptest.Server) {
317 | return mock.NewHTTPClient(
318 | mock.Endpoint{
319 | Path: qrCodesPath,
320 | Method: "GET",
321 | Response: allQRCodesResp,
322 | },
323 | )
324 | },
325 | ExpectError: false,
326 | ExpectedResult: allQRCodesResp,
327 | },
328 | {
329 | Name: "invalid parameters - caught by SDK",
330 | Request: map[string]interface{}{
331 | "count": float64(-1),
332 | },
333 | MockHttpClient: func() (*http.Client, *httptest.Server) {
334 | return mock.NewHTTPClient(
335 | mock.Endpoint{
336 | Path: qrCodesPath,
337 | Method: "GET",
338 | Response: map[string]interface{}{
339 | "error": map[string]interface{}{
340 | "code": "BAD_REQUEST_ERROR",
341 | "description": "The count value should be greater than or equal to 1",
342 | },
343 | },
344 | },
345 | )
346 | },
347 | ExpectError: true,
348 | ExpectedErrMsg: "fetching QR codes failed: " +
349 | "The count value should be greater than or equal to 1",
350 | },
351 | {
352 | Name: "validator error - invalid count parameter type",
353 | Request: map[string]interface{}{
354 | "count": "not-a-number",
355 | },
356 | MockHttpClient: nil,
357 | ExpectError: true,
358 | ExpectedErrMsg: "Validation errors",
359 | },
360 | {
361 | Name: "API error response",
362 | Request: map[string]interface{}{
363 | "count": float64(1000),
364 | },
365 | MockHttpClient: func() (*http.Client, *httptest.Server) {
366 | return mock.NewHTTPClient(
367 | mock.Endpoint{
368 | Path: qrCodesPath,
369 | Method: "GET",
370 | Response: errorResp,
371 | },
372 | )
373 | },
374 | ExpectError: true,
375 | ExpectedErrMsg: "fetching QR codes failed: The query parameters are invalid",
376 | },
377 | }
378 |
379 | for _, tc := range tests {
380 | t.Run(tc.Name, func(t *testing.T) {
381 | runToolTest(t, tc, FetchAllQRCodes, "QR Codes")
382 | })
383 | }
384 | }
385 |
386 | func Test_FetchQRCodesByCustomerID(t *testing.T) {
387 | qrCodesPath := fmt.Sprintf(
388 | "/%s%s",
389 | constants.VERSION_V1,
390 | constants.QRCODE_URL,
391 | )
392 |
393 | customerQRCodesResp := map[string]interface{}{
394 | "entity": "collection",
395 | "count": float64(1),
396 | "items": []interface{}{
397 | map[string]interface{}{
398 | "id": "qr_HMsgvioW64f0vh",
399 | "entity": "qr_code",
400 | "created_at": float64(1623660959),
401 | "name": "Store_1",
402 | "usage": "single_use",
403 | "type": "upi_qr",
404 | "image_url": "https://rzp.io/i/DTa2eQR",
405 | "payment_amount": float64(300),
406 | "status": "active",
407 | "description": "For Store 1",
408 | "fixed_amount": true,
409 | "payments_amount_received": float64(0),
410 | "payments_count_received": float64(0),
411 | "notes": map[string]interface{}{
412 | "purpose": "Test UPI QR Code notes",
413 | },
414 | "customer_id": "cust_HKsR5se84c5LTO",
415 | "close_by": float64(1681615838),
416 | "closed_at": nil,
417 | "close_reason": nil,
418 | },
419 | },
420 | }
421 |
422 | errorResp := map[string]interface{}{
423 | "error": map[string]interface{}{
424 | "code": "BAD_REQUEST_ERROR",
425 | "description": "The id provided is not a valid id",
426 | },
427 | }
428 |
429 | tests := []RazorpayToolTestCase{
430 | {
431 | Name: "successful fetch QR codes by customer ID",
432 | Request: map[string]interface{}{
433 | "customer_id": "cust_HKsR5se84c5LTO",
434 | },
435 | MockHttpClient: func() (*http.Client, *httptest.Server) {
436 | return mock.NewHTTPClient(
437 | mock.Endpoint{
438 | Path: qrCodesPath,
439 | Method: "GET",
440 | Response: customerQRCodesResp,
441 | },
442 | )
443 | },
444 | ExpectError: false,
445 | ExpectedResult: customerQRCodesResp,
446 | },
447 | {
448 | Name: "missing required customer_id parameter",
449 | Request: map[string]interface{}{},
450 | MockHttpClient: nil,
451 | ExpectError: true,
452 | ExpectedErrMsg: "missing required parameter: customer_id",
453 | },
454 | {
455 | Name: "validator error - invalid customer_id parameter type",
456 | Request: map[string]interface{}{
457 | "customer_id": 12345,
458 | },
459 | MockHttpClient: nil,
460 | ExpectError: true,
461 | ExpectedErrMsg: "invalid parameter type: customer_id",
462 | },
463 | {
464 | Name: "API error - invalid customer ID",
465 | Request: map[string]interface{}{
466 | "customer_id": "invalid_customer_id",
467 | },
468 | MockHttpClient: func() (*http.Client, *httptest.Server) {
469 | return mock.NewHTTPClient(
470 | mock.Endpoint{
471 | Path: qrCodesPath,
472 | Method: "GET",
473 | Response: errorResp,
474 | },
475 | )
476 | },
477 | ExpectError: true,
478 | ExpectedErrMsg: "fetching QR codes failed: " +
479 | "The id provided is not a valid id",
480 | },
481 | }
482 |
483 | for _, tc := range tests {
484 | t.Run(tc.Name, func(t *testing.T) {
485 | runToolTest(t, tc, FetchQRCodesByCustomerID, "QR Codes by Customer ID")
486 | })
487 | }
488 | }
489 |
490 | func Test_FetchQRCodesByPaymentID(t *testing.T) {
491 | qrCodesPath := fmt.Sprintf(
492 | "/%s%s",
493 | constants.VERSION_V1,
494 | constants.QRCODE_URL,
495 | )
496 |
497 | paymentQRCodesResp := map[string]interface{}{
498 | "entity": "collection",
499 | "count": float64(1),
500 | "items": []interface{}{
501 | map[string]interface{}{
502 | "id": "qr_HMsqRoeVwKbwAF",
503 | "entity": "qr_code",
504 | "created_at": float64(1623661499),
505 | "name": "Fresh Groceries",
506 | "usage": "multiple_use",
507 | "type": "upi_qr",
508 | "image_url": "https://rzp.io/i/eI9XD54Q",
509 | "payment_amount": nil,
510 | "status": "active",
511 | "description": "Buy fresh groceries",
512 | "fixed_amount": false,
513 | "payments_amount_received": float64(1000),
514 | "payments_count_received": float64(1),
515 | "notes": []interface{}{},
516 | "customer_id": "cust_HKsR5se84c5LTO",
517 | "close_by": float64(1624472999),
518 | "close_reason": nil,
519 | },
520 | },
521 | }
522 |
523 | errorResp := map[string]interface{}{
524 | "error": map[string]interface{}{
525 | "code": "BAD_REQUEST_ERROR",
526 | "description": "The id provided is not a valid id",
527 | },
528 | }
529 |
530 | tests := []RazorpayToolTestCase{
531 | {
532 | Name: "successful fetch QR codes by payment ID",
533 | Request: map[string]interface{}{
534 | "payment_id": "pay_Di5iqCqA1WEHq6",
535 | },
536 | MockHttpClient: func() (*http.Client, *httptest.Server) {
537 | return mock.NewHTTPClient(
538 | mock.Endpoint{
539 | Path: qrCodesPath,
540 | Method: "GET",
541 | Response: paymentQRCodesResp,
542 | },
543 | )
544 | },
545 | ExpectError: false,
546 | ExpectedResult: paymentQRCodesResp,
547 | },
548 | {
549 | Name: "missing required payment_id parameter",
550 | Request: map[string]interface{}{},
551 | MockHttpClient: nil,
552 | ExpectError: true,
553 | ExpectedErrMsg: "missing required parameter: payment_id",
554 | },
555 | {
556 | Name: "validator error - invalid payment_id parameter type",
557 | Request: map[string]interface{}{
558 | "payment_id": 12345,
559 | },
560 | MockHttpClient: nil,
561 | ExpectError: true,
562 | ExpectedErrMsg: "invalid parameter type: payment_id",
563 | },
564 | {
565 | Name: "API error - invalid payment ID",
566 | Request: map[string]interface{}{
567 | "payment_id": "invalid_payment_id",
568 | },
569 | MockHttpClient: func() (*http.Client, *httptest.Server) {
570 | return mock.NewHTTPClient(
571 | mock.Endpoint{
572 | Path: qrCodesPath,
573 | Method: "GET",
574 | Response: errorResp,
575 | },
576 | )
577 | },
578 | ExpectError: true,
579 | ExpectedErrMsg: "fetching QR codes failed: " +
580 | "The id provided is not a valid id",
581 | },
582 | }
583 |
584 | for _, tc := range tests {
585 | t.Run(tc.Name, func(t *testing.T) {
586 | runToolTest(t, tc, FetchQRCodesByPaymentID, "QR Codes by Payment ID")
587 | })
588 | }
589 | }
590 |
591 | func TestFetchQRCode(t *testing.T) {
592 | // Initialize necessary variables
593 | qrID := "qr_FuZIYx6rMbP6gs"
594 | apiPath := fmt.Sprintf(
595 | "/%s%s/%s",
596 | constants.VERSION_V1,
597 | constants.QRCODE_URL,
598 | qrID,
599 | )
600 |
601 | // Successful response based on Razorpay docs
602 | successResponse := map[string]interface{}{
603 | "id": qrID,
604 | "entity": "qr_code",
605 | "created_at": float64(1623915088),
606 | "name": "Store_1",
607 | "usage": "single_use",
608 | "type": "upi_qr",
609 | "image_url": "https://rzp.io/i/oCswTOcCo",
610 | "payment_amount": float64(300),
611 | "status": "active",
612 | "description": "For Store 1",
613 | "fixed_amount": true,
614 | "payments_amount_received": float64(0),
615 | "payments_count_received": float64(0),
616 | "notes": map[string]interface{}{
617 | "purpose": "Test UPI QR Code notes",
618 | },
619 | "customer_id": "cust_HKsR5se84c5LTO",
620 | "close_by": float64(1681615838),
621 | "closed_at": nil,
622 | "close_reason": nil,
623 | }
624 |
625 | errorResp := map[string]interface{}{
626 | "error": map[string]interface{}{
627 | "code": "BAD_REQUEST_ERROR",
628 | "description": "The QR code ID provided is invalid",
629 | },
630 | }
631 |
632 | tests := []RazorpayToolTestCase{
633 | {
634 | Name: "successful fetch QR code by ID",
635 | Request: map[string]interface{}{
636 | "qr_code_id": qrID,
637 | },
638 | MockHttpClient: func() (*http.Client, *httptest.Server) {
639 | return mock.NewHTTPClient(
640 | mock.Endpoint{
641 | Path: apiPath,
642 | Method: "GET",
643 | Response: successResponse,
644 | },
645 | )
646 | },
647 | ExpectError: false,
648 | ExpectedResult: successResponse,
649 | },
650 | {
651 | Name: "missing required qr_code_id parameter",
652 | Request: map[string]interface{}{},
653 | MockHttpClient: nil,
654 | ExpectError: true,
655 | ExpectedErrMsg: "missing required parameter: qr_code_id",
656 | },
657 | {
658 | Name: "validator error - invalid qr_code_id parameter type",
659 | Request: map[string]interface{}{
660 | "qr_code_id": 12345,
661 | },
662 | MockHttpClient: nil,
663 | ExpectError: true,
664 | ExpectedErrMsg: "invalid parameter type: qr_code_id",
665 | },
666 | {
667 | Name: "API error - invalid QR code ID",
668 | Request: map[string]interface{}{
669 | "qr_code_id": qrID,
670 | },
671 | MockHttpClient: func() (*http.Client, *httptest.Server) {
672 | return mock.NewHTTPClient(
673 | mock.Endpoint{
674 | Path: apiPath,
675 | Method: "GET",
676 | Response: errorResp,
677 | },
678 | )
679 | },
680 | ExpectError: true,
681 | ExpectedErrMsg: "fetching QR code failed: " +
682 | "The QR code ID provided is invalid",
683 | },
684 | }
685 |
686 | for _, tc := range tests {
687 | t.Run(tc.Name, func(t *testing.T) {
688 | runToolTest(t, tc, FetchQRCode, "QR Code")
689 | })
690 | }
691 | }
692 |
693 | func TestFetchPaymentsForQRCode(t *testing.T) {
694 | apiPath := "/" + constants.VERSION_V1 +
695 | constants.QRCODE_URL + "/qr_test123/payments"
696 |
697 | successResponse := map[string]interface{}{
698 | "entity": "collection",
699 | "count": float64(2),
700 | "items": []interface{}{
701 | map[string]interface{}{
702 | "id": "pay_test123",
703 | "entity": "payment",
704 | "amount": float64(500),
705 | "currency": "INR",
706 | "status": "captured",
707 | "method": "upi",
708 | "amount_refunded": float64(0),
709 | "refund_status": nil,
710 | "captured": true,
711 | "description": "QRv2 Payment",
712 | "customer_id": "cust_test123",
713 | "created_at": float64(1623662800),
714 | },
715 | map[string]interface{}{
716 | "id": "pay_test456",
717 | "entity": "payment",
718 | "amount": float64(1000),
719 | "currency": "INR",
720 | "status": "refunded",
721 | "method": "upi",
722 | "amount_refunded": float64(1000),
723 | "refund_status": "full",
724 | "captured": true,
725 | "description": "QRv2 Payment",
726 | "customer_id": "cust_test123",
727 | "created_at": float64(1623661533),
728 | },
729 | },
730 | }
731 |
732 | tests := []RazorpayToolTestCase{
733 | {
734 | Name: "successful fetch payments for QR code",
735 | Request: map[string]interface{}{
736 | "qr_code_id": "qr_test123",
737 | "count": 10,
738 | "from": 1623661000,
739 | "to": 1623663000,
740 | "skip": 0,
741 | },
742 | MockHttpClient: func() (*http.Client, *httptest.Server) {
743 | return mock.NewHTTPClient(
744 | mock.Endpoint{
745 | Path: apiPath,
746 | Method: "GET",
747 | Response: successResponse,
748 | },
749 | )
750 | },
751 | ExpectError: false,
752 | ExpectedResult: successResponse,
753 | },
754 | {
755 | Name: "missing required parameter",
756 | Request: map[string]interface{}{
757 | "count": 10,
758 | },
759 | MockHttpClient: nil,
760 | ExpectError: true,
761 | ExpectedErrMsg: "missing required parameter: qr_code_id",
762 | },
763 | {
764 | Name: "invalid parameter type",
765 | Request: map[string]interface{}{
766 | "qr_code_id": 123,
767 | },
768 | MockHttpClient: nil,
769 | ExpectError: true,
770 | ExpectedErrMsg: "invalid parameter type: qr_code_id",
771 | },
772 | {
773 | Name: "API error",
774 | Request: map[string]interface{}{
775 | "qr_code_id": "qr_test123",
776 | },
777 | MockHttpClient: func() (*http.Client, *httptest.Server) {
778 | return mock.NewHTTPClient(
779 | mock.Endpoint{
780 | Path: apiPath,
781 | Method: "GET",
782 | Response: map[string]interface{}{
783 | "error": map[string]interface{}{
784 | "code": "BAD_REQUEST_ERROR",
785 | "description": "mock error",
786 | },
787 | },
788 | },
789 | )
790 | },
791 | ExpectError: true,
792 | ExpectedErrMsg: "fetching payments for QR code failed: mock error",
793 | },
794 | }
795 |
796 | for _, tc := range tests {
797 | t.Run(tc.Name, func(t *testing.T) {
798 | runToolTest(t, tc, FetchPaymentsForQRCode, "QR Code Payments")
799 | })
800 | }
801 | }
802 |
803 | func TestCloseQRCode(t *testing.T) {
804 | successResponse := map[string]interface{}{
805 | "id": "qr_HMsVL8HOpbMcjU",
806 | "entity": "qr_code",
807 | "created_at": float64(1623660301),
808 | "name": "Store_1",
809 | "usage": "single_use",
810 | "type": "upi_qr",
811 | "image_url": "https://rzp.io/i/BWcUVrLp",
812 | "payment_amount": float64(300),
813 | "status": "closed",
814 | "description": "For Store 1",
815 | "fixed_amount": true,
816 | "payments_amount_received": float64(0),
817 | "payments_count_received": float64(0),
818 | "notes": map[string]interface{}{
819 | "purpose": "Test UPI QR Code notes",
820 | },
821 | "customer_id": "cust_HKsR5se84c5LTO",
822 | "close_by": float64(1681615838),
823 | "closed_at": float64(1623660445),
824 | "close_reason": "on_demand",
825 | }
826 |
827 | baseAPIPath := fmt.Sprintf("/%s%s", constants.VERSION_V1, constants.QRCODE_URL)
828 | qrCodeID := "qr_HMsVL8HOpbMcjU"
829 | apiPath := fmt.Sprintf("%s/%s/close", baseAPIPath, qrCodeID)
830 |
831 | tests := []RazorpayToolTestCase{
832 | {
833 | Name: "successful close QR code",
834 | Request: map[string]interface{}{
835 | "qr_code_id": qrCodeID,
836 | },
837 | MockHttpClient: func() (*http.Client, *httptest.Server) {
838 | return mock.NewHTTPClient(
839 | mock.Endpoint{
840 | Path: apiPath,
841 | Method: "POST",
842 | Response: successResponse,
843 | },
844 | )
845 | },
846 | ExpectError: false,
847 | ExpectedResult: successResponse,
848 | },
849 | {
850 | Name: "missing required qr_code_id parameter",
851 | Request: map[string]interface{}{},
852 | MockHttpClient: nil,
853 | ExpectError: true,
854 | ExpectedErrMsg: "missing required parameter: qr_code_id",
855 | },
856 | }
857 |
858 | for _, tc := range tests {
859 | t.Run(tc.Name, func(t *testing.T) {
860 | runToolTest(t, tc, CloseQRCode, "QR Code")
861 | })
862 | }
863 | }
864 |
```