This is page 4 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/orders_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_CreateOrder(t *testing.T) {
15 | createOrderPath := fmt.Sprintf(
16 | "/%s%s",
17 | constants.VERSION_V1,
18 | constants.ORDER_URL,
19 | )
20 |
21 | // Define common response maps to be reused
22 | orderWithAllParamsResp := map[string]interface{}{
23 | "id": "order_EKwxwAgItmmXdp",
24 | "amount": float64(10000),
25 | "currency": "INR",
26 | "receipt": "receipt-123",
27 | "partial_payment": true,
28 | "first_payment_min_amount": float64(5000),
29 | "notes": map[string]interface{}{
30 | "customer_name": "test-customer",
31 | "product_name": "test-product",
32 | },
33 | "status": "created",
34 | }
35 |
36 | orderWithRequiredParamsResp := map[string]interface{}{
37 | "id": "order_EKwxwAgItmmXdp",
38 | "amount": float64(10000),
39 | "currency": "INR",
40 | "status": "created",
41 | }
42 |
43 | errorResp := map[string]interface{}{
44 | "error": map[string]interface{}{
45 | "code": "BAD_REQUEST_ERROR",
46 | "description": "Razorpay API error: Bad request",
47 | },
48 | }
49 |
50 | tests := []RazorpayToolTestCase{
51 | {
52 | Name: "successful order creation with all parameters",
53 | Request: map[string]interface{}{
54 | "amount": float64(10000),
55 | "currency": "INR",
56 | "receipt": "receipt-123",
57 | "partial_payment": true,
58 | "first_payment_min_amount": float64(5000),
59 | "notes": map[string]interface{}{
60 | "customer_name": "test-customer",
61 | "product_name": "test-product",
62 | },
63 | },
64 | MockHttpClient: func() (*http.Client, *httptest.Server) {
65 | return mock.NewHTTPClient(
66 | mock.Endpoint{
67 | Path: createOrderPath,
68 | Method: "POST",
69 | Response: orderWithAllParamsResp,
70 | },
71 | )
72 | },
73 | ExpectError: false,
74 | ExpectedResult: orderWithAllParamsResp,
75 | },
76 | {
77 | Name: "successful order creation with required params only",
78 | Request: map[string]interface{}{
79 | "amount": float64(10000),
80 | "currency": "INR",
81 | },
82 | MockHttpClient: func() (*http.Client, *httptest.Server) {
83 | return mock.NewHTTPClient(
84 | mock.Endpoint{
85 | Path: createOrderPath,
86 | Method: "POST",
87 | Response: orderWithRequiredParamsResp,
88 | },
89 | )
90 | },
91 | ExpectError: false,
92 | ExpectedResult: orderWithRequiredParamsResp,
93 | },
94 | {
95 | Name: "multiple validation errors",
96 | Request: map[string]interface{}{
97 | // Missing both amount and currency (required parameters)
98 | "partial_payment": "invalid_boolean", // Wrong type for boolean
99 | "first_payment_min_amount": "invalid_number", // Wrong type for number
100 | },
101 | MockHttpClient: nil, // No HTTP client needed for validation error
102 | ExpectError: true,
103 | ExpectedErrMsg: "Validation errors:\n- " +
104 | "missing required parameter: amount\n- " +
105 | "missing required parameter: currency\n- " +
106 | "invalid parameter type: partial_payment",
107 | },
108 | {
109 | Name: "first_payment_min_amount validation when partial_payment is true",
110 | Request: map[string]interface{}{
111 | "amount": float64(10000),
112 | "currency": "INR",
113 | "partial_payment": true,
114 | "first_payment_min_amount": "invalid_number",
115 | },
116 | MockHttpClient: nil, // No HTTP client needed for validation error
117 | ExpectError: true,
118 | ExpectedErrMsg: "Validation errors:\n- " +
119 | "invalid parameter type: first_payment_min_amount",
120 | },
121 | {
122 | Name: "order creation fails",
123 | Request: map[string]interface{}{
124 | "amount": float64(10000),
125 | "currency": "INR",
126 | },
127 | MockHttpClient: func() (*http.Client, *httptest.Server) {
128 | return mock.NewHTTPClient(
129 | mock.Endpoint{
130 | Path: createOrderPath,
131 | Method: "POST",
132 | Response: errorResp,
133 | },
134 | )
135 | },
136 | ExpectError: true,
137 | ExpectedErrMsg: "creating order failed: Razorpay API error: Bad request",
138 | },
139 | {
140 | Name: "successful SBMD mandate order creation",
141 | Request: map[string]interface{}{
142 | "amount": float64(500000),
143 | "currency": "INR",
144 | "customer_id": "cust_4xbQrmEoA5WJ01",
145 | "method": "upi",
146 | "token": map[string]interface{}{
147 | "max_amount": float64(500000),
148 | "expire_at": float64(2709971120),
149 | "frequency": "as_presented",
150 | "type": "single_block_multiple_debit",
151 | },
152 | "receipt": "Receipt No. 1",
153 | "notes": map[string]interface{}{
154 | "notes_key_1": "Tea, Earl Grey, Hot",
155 | "notes_key_2": "Tea, Earl Grey... decaf.",
156 | },
157 | },
158 | MockHttpClient: func() (*http.Client, *httptest.Server) {
159 | sbmdOrderResp := map[string]interface{}{
160 | "id": "order_SBMD123456",
161 | "amount": float64(500000),
162 | "currency": "INR",
163 | "customer_id": "cust_4xbQrmEoA5WJ01",
164 | "method": "upi",
165 | "token": map[string]interface{}{
166 | "max_amount": float64(500000),
167 | "expire_at": float64(2709971120),
168 | "frequency": "as_presented",
169 | "type": "single_block_multiple_debit",
170 | },
171 | "receipt": "Receipt No. 1",
172 | "status": "created",
173 | "notes": map[string]interface{}{
174 | "notes_key_1": "Tea, Earl Grey, Hot",
175 | "notes_key_2": "Tea, Earl Grey... decaf.",
176 | },
177 | }
178 | return mock.NewHTTPClient(
179 | mock.Endpoint{
180 | Path: createOrderPath,
181 | Method: "POST",
182 | Response: sbmdOrderResp,
183 | },
184 | )
185 | },
186 | ExpectError: false,
187 | ExpectedResult: map[string]interface{}{
188 | "id": "order_SBMD123456",
189 | "amount": float64(500000),
190 | "currency": "INR",
191 | "customer_id": "cust_4xbQrmEoA5WJ01",
192 | "method": "upi",
193 | "token": map[string]interface{}{
194 | "max_amount": float64(500000),
195 | "expire_at": float64(2709971120),
196 | "frequency": "as_presented",
197 | "type": "single_block_multiple_debit",
198 | },
199 | "receipt": "Receipt No. 1",
200 | "status": "created",
201 | "notes": map[string]interface{}{
202 | "notes_key_1": "Tea, Earl Grey, Hot",
203 | "notes_key_2": "Tea, Earl Grey... decaf.",
204 | },
205 | },
206 | },
207 | {
208 | Name: "mandate order with invalid token parameter type",
209 | Request: map[string]interface{}{
210 | "amount": float64(500000),
211 | "currency": "INR",
212 | "customer_id": "cust_4xbQrmEoA5WJ01",
213 | "method": "upi",
214 | "token": "invalid_token_should_be_object",
215 | },
216 | MockHttpClient: nil,
217 | ExpectError: true,
218 | ExpectedErrMsg: "invalid parameter type: token",
219 | },
220 | {
221 | Name: "mandate order with invalid method parameter type",
222 | Request: map[string]interface{}{
223 | "amount": float64(500000),
224 | "currency": "INR",
225 | "customer_id": "cust_4xbQrmEoA5WJ01",
226 | "method": 123,
227 | "token": map[string]interface{}{
228 | "max_amount": float64(500000),
229 | "expire_at": float64(2709971120),
230 | "frequency": "as_presented",
231 | },
232 | },
233 | MockHttpClient: nil,
234 | ExpectError: true,
235 | ExpectedErrMsg: "invalid parameter type: method",
236 | },
237 | {
238 | Name: "token validation - missing max_amount",
239 | Request: map[string]interface{}{
240 | "amount": float64(500000),
241 | "currency": "INR",
242 | "customer_id": "cust_4xbQrmEoA5WJ01",
243 | "method": "upi",
244 | "token": map[string]interface{}{
245 | "expire_at": float64(2709971120),
246 | "frequency": "as_presented",
247 | },
248 | },
249 | MockHttpClient: nil,
250 | ExpectError: true,
251 | ExpectedErrMsg: "token.max_amount is required",
252 | },
253 | {
254 | Name: "token validation - missing frequency",
255 | Request: map[string]interface{}{
256 | "amount": float64(500000),
257 | "currency": "INR",
258 | "customer_id": "cust_4xbQrmEoA5WJ01",
259 | "method": "upi",
260 | "token": map[string]interface{}{
261 | "max_amount": float64(500000),
262 | "expire_at": float64(2709971120),
263 | },
264 | },
265 | MockHttpClient: nil,
266 | ExpectError: true,
267 | ExpectedErrMsg: "token.frequency is required",
268 | },
269 | {
270 | Name: "token validation - invalid max_amount type",
271 | Request: map[string]interface{}{
272 | "amount": float64(500000),
273 | "currency": "INR",
274 | "customer_id": "cust_4xbQrmEoA5WJ01",
275 | "method": "upi",
276 | "token": map[string]interface{}{
277 | "max_amount": "invalid_string",
278 | "expire_at": float64(2709971120),
279 | "frequency": "as_presented",
280 | },
281 | },
282 | MockHttpClient: nil,
283 | ExpectError: true,
284 | ExpectedErrMsg: "token.max_amount must be a number",
285 | },
286 | {
287 | Name: "token validation - invalid max_amount value",
288 | Request: map[string]interface{}{
289 | "amount": float64(500000),
290 | "currency": "INR",
291 | "customer_id": "cust_4xbQrmEoA5WJ01",
292 | "method": "upi",
293 | "token": map[string]interface{}{
294 | "max_amount": float64(-100),
295 | "expire_at": float64(2709971120),
296 | "frequency": "as_presented",
297 | },
298 | },
299 | MockHttpClient: nil,
300 | ExpectError: true,
301 | ExpectedErrMsg: "token.max_amount must be greater than 0",
302 | },
303 | {
304 | Name: "token validation - invalid expire_at type",
305 | Request: map[string]interface{}{
306 | "amount": float64(500000),
307 | "currency": "INR",
308 | "customer_id": "cust_4xbQrmEoA5WJ01",
309 | "method": "upi",
310 | "token": map[string]interface{}{
311 | "max_amount": float64(500000),
312 | "expire_at": "invalid_string",
313 | "frequency": "as_presented",
314 | },
315 | },
316 | MockHttpClient: nil,
317 | ExpectError: true,
318 | ExpectedErrMsg: "token.expire_at must be a number",
319 | },
320 | {
321 | Name: "token validation - invalid expire_at value",
322 | Request: map[string]interface{}{
323 | "amount": float64(500000),
324 | "currency": "INR",
325 | "customer_id": "cust_4xbQrmEoA5WJ01",
326 | "method": "upi",
327 | "token": map[string]interface{}{
328 | "max_amount": float64(500000),
329 | "expire_at": float64(-100),
330 | "frequency": "as_presented",
331 | },
332 | },
333 | MockHttpClient: nil,
334 | ExpectError: true,
335 | ExpectedErrMsg: "token.expire_at must be greater than 0",
336 | },
337 | {
338 | Name: "token validation - invalid frequency type",
339 | Request: map[string]interface{}{
340 | "amount": float64(500000),
341 | "currency": "INR",
342 | "customer_id": "cust_4xbQrmEoA5WJ01",
343 | "method": "upi",
344 | "token": map[string]interface{}{
345 | "max_amount": float64(500000),
346 | "expire_at": float64(2709971120),
347 | "frequency": 123,
348 | },
349 | },
350 | MockHttpClient: nil,
351 | ExpectError: true,
352 | ExpectedErrMsg: "token.frequency must be a string",
353 | },
354 | {
355 | Name: "token validation - invalid frequency value",
356 | Request: map[string]interface{}{
357 | "amount": float64(500000),
358 | "currency": "INR",
359 | "customer_id": "cust_4xbQrmEoA5WJ01",
360 | "method": "upi",
361 | "token": map[string]interface{}{
362 | "max_amount": float64(500000),
363 | "expire_at": float64(2709971120),
364 | "frequency": "invalid_frequency",
365 | },
366 | },
367 | MockHttpClient: nil,
368 | ExpectError: true,
369 | ExpectedErrMsg: "token.frequency must be one of: as_presented, " +
370 | "monthly, one_time, yearly, weekly, daily",
371 | },
372 | {
373 | Name: "token validation - invalid type value",
374 | Request: map[string]interface{}{
375 | "amount": float64(500000),
376 | "currency": "INR",
377 | "customer_id": "cust_4xbQrmEoA5WJ01",
378 | "method": "upi",
379 | "token": map[string]interface{}{
380 | "max_amount": float64(500000),
381 | "expire_at": float64(2709971120),
382 | "frequency": "as_presented",
383 | "type": "invalid_type",
384 | },
385 | },
386 | MockHttpClient: nil,
387 | ExpectError: true,
388 | ExpectedErrMsg: "token.type must be one of: single_block_multiple_debit",
389 | },
390 | {
391 | Name: "token validation - invalid type type",
392 | Request: map[string]interface{}{
393 | "amount": float64(500000),
394 | "currency": "INR",
395 | "customer_id": "cust_4xbQrmEoA5WJ01",
396 | "method": "upi",
397 | "token": map[string]interface{}{
398 | "max_amount": float64(500000),
399 | "expire_at": float64(2709971120),
400 | "frequency": "as_presented",
401 | "type": 123,
402 | },
403 | },
404 | MockHttpClient: nil,
405 | ExpectError: true,
406 | ExpectedErrMsg: "token.type must be a string",
407 | },
408 | {
409 | Name: "token validation - missing type",
410 | Request: map[string]interface{}{
411 | "amount": float64(500000),
412 | "currency": "INR",
413 | "customer_id": "cust_4xbQrmEoA5WJ01",
414 | "method": "upi",
415 | "token": map[string]interface{}{
416 | "max_amount": float64(500000),
417 | "expire_at": float64(2709971120),
418 | "frequency": "as_presented",
419 | },
420 | },
421 | MockHttpClient: nil,
422 | ExpectError: true,
423 | ExpectedErrMsg: "token.type is required",
424 | },
425 | {
426 | Name: "token validation - default expire_at when not provided",
427 | Request: map[string]interface{}{
428 | "amount": float64(500000),
429 | "currency": "INR",
430 | "customer_id": "cust_4xbQrmEoA5WJ01",
431 | "method": "upi",
432 | "token": map[string]interface{}{
433 | "max_amount": float64(500000),
434 | "frequency": "as_presented",
435 | "type": "single_block_multiple_debit",
436 | },
437 | },
438 | MockHttpClient: func() (*http.Client, *httptest.Server) {
439 | return mock.NewHTTPClient(
440 | mock.Endpoint{
441 | Path: createOrderPath,
442 | Method: "POST",
443 | Response: map[string]interface{}{
444 | "id": "order_test_12345",
445 | },
446 | },
447 | )
448 | },
449 | ExpectError: false,
450 | ExpectedResult: map[string]interface{}{
451 | "id": "order_test_12345",
452 | },
453 | },
454 | }
455 |
456 | for _, tc := range tests {
457 | t.Run(tc.Name, func(t *testing.T) {
458 | runToolTest(t, tc, CreateOrder, "Order")
459 | })
460 | }
461 | }
462 |
463 | func Test_FetchOrder(t *testing.T) {
464 | fetchOrderPathFmt := fmt.Sprintf(
465 | "/%s%s/%%s",
466 | constants.VERSION_V1,
467 | constants.ORDER_URL,
468 | )
469 |
470 | orderResp := map[string]interface{}{
471 | "id": "order_EKwxwAgItmmXdp",
472 | "amount": float64(10000),
473 | "currency": "INR",
474 | "receipt": "receipt-123",
475 | "status": "created",
476 | }
477 |
478 | orderNotFoundResp := map[string]interface{}{
479 | "error": map[string]interface{}{
480 | "code": "BAD_REQUEST_ERROR",
481 | "description": "order not found",
482 | },
483 | }
484 |
485 | tests := []RazorpayToolTestCase{
486 | {
487 | Name: "successful order fetch",
488 | Request: map[string]interface{}{
489 | "order_id": "order_EKwxwAgItmmXdp",
490 | },
491 | MockHttpClient: func() (*http.Client, *httptest.Server) {
492 | return mock.NewHTTPClient(
493 | mock.Endpoint{
494 | Path: fmt.Sprintf(fetchOrderPathFmt, "order_EKwxwAgItmmXdp"),
495 | Method: "GET",
496 | Response: orderResp,
497 | },
498 | )
499 | },
500 | ExpectError: false,
501 | ExpectedResult: orderResp,
502 | },
503 | {
504 | Name: "order not found",
505 | Request: map[string]interface{}{
506 | "order_id": "order_invalid",
507 | },
508 | MockHttpClient: func() (*http.Client, *httptest.Server) {
509 | return mock.NewHTTPClient(
510 | mock.Endpoint{
511 | Path: fmt.Sprintf(fetchOrderPathFmt, "order_invalid"),
512 | Method: "GET",
513 | Response: orderNotFoundResp,
514 | },
515 | )
516 | },
517 | ExpectError: true,
518 | ExpectedErrMsg: "fetching order failed: order not found",
519 | },
520 | {
521 | Name: "missing order_id parameter",
522 | Request: map[string]interface{}{},
523 | MockHttpClient: nil, // No HTTP client needed for validation error
524 | ExpectError: true,
525 | ExpectedErrMsg: "missing required parameter: order_id",
526 | },
527 | }
528 |
529 | for _, tc := range tests {
530 | t.Run(tc.Name, func(t *testing.T) {
531 | runToolTest(t, tc, FetchOrder, "Order")
532 | })
533 | }
534 | }
535 |
536 | func Test_FetchAllOrders(t *testing.T) {
537 | fetchAllOrdersPath := fmt.Sprintf(
538 | "/%s%s",
539 | constants.VERSION_V1,
540 | constants.ORDER_URL,
541 | )
542 |
543 | // Define the sample response for all orders
544 | ordersResp := map[string]interface{}{
545 | "entity": "collection",
546 | "count": float64(2),
547 | "items": []interface{}{
548 | map[string]interface{}{
549 | "id": "order_EKzX2WiEWbMxmx",
550 | "entity": "order",
551 | "amount": float64(1234),
552 | "amount_paid": float64(0),
553 | "amount_due": float64(1234),
554 | "currency": "INR",
555 | "receipt": "Receipt No. 1",
556 | "offer_id": nil,
557 | "status": "created",
558 | "attempts": float64(0),
559 | "notes": []interface{}{},
560 | "created_at": float64(1582637108),
561 | },
562 | map[string]interface{}{
563 | "id": "order_EAI5nRfThga2TU",
564 | "entity": "order",
565 | "amount": float64(100),
566 | "amount_paid": float64(0),
567 | "amount_due": float64(100),
568 | "currency": "INR",
569 | "receipt": "Receipt No. 1",
570 | "offer_id": nil,
571 | "status": "created",
572 | "attempts": float64(0),
573 | "notes": []interface{}{},
574 | "created_at": float64(1580300731),
575 | },
576 | },
577 | }
578 |
579 | // Define error response
580 | errorResp := map[string]interface{}{
581 | "error": map[string]interface{}{
582 | "code": "BAD_REQUEST_ERROR",
583 | "description": "Razorpay API error: Bad request",
584 | },
585 | }
586 |
587 | // Define the test cases
588 | tests := []RazorpayToolTestCase{
589 | {
590 | Name: "successful fetch all orders with no parameters",
591 | Request: map[string]interface{}{},
592 | MockHttpClient: func() (*http.Client, *httptest.Server) {
593 | return mock.NewHTTPClient(
594 | mock.Endpoint{
595 | Path: fetchAllOrdersPath,
596 | Method: "GET",
597 | Response: ordersResp,
598 | },
599 | )
600 | },
601 | ExpectError: false,
602 | ExpectedResult: ordersResp,
603 | },
604 | {
605 | Name: "successful fetch all orders with pagination",
606 | Request: map[string]interface{}{
607 | "count": 2,
608 | "skip": 1,
609 | },
610 | MockHttpClient: func() (*http.Client, *httptest.Server) {
611 | return mock.NewHTTPClient(
612 | mock.Endpoint{
613 | Path: fetchAllOrdersPath,
614 | Method: "GET",
615 | Response: ordersResp,
616 | },
617 | )
618 | },
619 | ExpectError: false,
620 | ExpectedResult: ordersResp,
621 | },
622 | {
623 | Name: "successful fetch all orders with time range",
624 | Request: map[string]interface{}{
625 | "from": 1580000000,
626 | "to": 1590000000,
627 | },
628 | MockHttpClient: func() (*http.Client, *httptest.Server) {
629 | return mock.NewHTTPClient(
630 | mock.Endpoint{
631 | Path: fetchAllOrdersPath,
632 | Method: "GET",
633 | Response: ordersResp,
634 | },
635 | )
636 | },
637 | ExpectError: false,
638 | ExpectedResult: ordersResp,
639 | },
640 | {
641 | Name: "successful fetch all orders with filtering",
642 | Request: map[string]interface{}{
643 | "authorized": 1,
644 | "receipt": "Receipt No. 1",
645 | },
646 | MockHttpClient: func() (*http.Client, *httptest.Server) {
647 | return mock.NewHTTPClient(
648 | mock.Endpoint{
649 | Path: fetchAllOrdersPath,
650 | Method: "GET",
651 | Response: ordersResp,
652 | },
653 | )
654 | },
655 | ExpectError: false,
656 | ExpectedResult: ordersResp,
657 | },
658 | {
659 | Name: "successful fetch all orders with expand",
660 | Request: map[string]interface{}{
661 | "expand": []interface{}{"payments"},
662 | },
663 | MockHttpClient: func() (*http.Client, *httptest.Server) {
664 | return mock.NewHTTPClient(
665 | mock.Endpoint{
666 | Path: fetchAllOrdersPath,
667 | Method: "GET",
668 | Response: ordersResp,
669 | },
670 | )
671 | },
672 | ExpectError: false,
673 | ExpectedResult: ordersResp,
674 | },
675 | {
676 | Name: "multiple validation errors",
677 | Request: map[string]interface{}{
678 | "count": "not-a-number",
679 | "skip": "not-a-number",
680 | "from": "not-a-number",
681 | "to": "not-a-number",
682 | "expand": "not-an-array",
683 | },
684 | MockHttpClient: nil, // No HTTP client needed for validation error
685 | ExpectError: true,
686 | ExpectedErrMsg: "Validation errors:\n- " +
687 | "invalid parameter type: count\n- " +
688 | "invalid parameter type: skip\n- " +
689 | "invalid parameter type: from\n- " +
690 | "invalid parameter type: to\n- " +
691 | "invalid parameter type: expand",
692 | },
693 | {
694 | Name: "fetch all orders fails",
695 | Request: map[string]interface{}{
696 | "count": 100,
697 | },
698 | MockHttpClient: func() (*http.Client, *httptest.Server) {
699 | return mock.NewHTTPClient(
700 | mock.Endpoint{
701 | Path: fetchAllOrdersPath,
702 | Method: "GET",
703 | Response: errorResp,
704 | },
705 | )
706 | },
707 | ExpectError: true,
708 | ExpectedErrMsg: "fetching orders failed: Razorpay API error: Bad request",
709 | },
710 | }
711 |
712 | for _, tc := range tests {
713 | t.Run(tc.Name, func(t *testing.T) {
714 | runToolTest(t, tc, FetchAllOrders, "Order")
715 | })
716 | }
717 | }
718 |
719 | func Test_FetchOrderPayments(t *testing.T) {
720 | fetchOrderPaymentsPathFmt := fmt.Sprintf(
721 | "/%s%s/%%s/payments",
722 | constants.VERSION_V1,
723 | constants.ORDER_URL,
724 | )
725 |
726 | // Define the sample response for order payments
727 | paymentsResp := map[string]interface{}{
728 | "entity": "collection",
729 | "count": float64(2),
730 | "items": []interface{}{
731 | map[string]interface{}{
732 | "id": "pay_N8FUmetkCE2hZP",
733 | "entity": "payment",
734 | "amount": float64(100),
735 | "currency": "INR",
736 | "status": "failed",
737 | "order_id": "order_N8FRN5zTm5S3wx",
738 | "invoice_id": nil,
739 | "international": false,
740 | "method": "upi",
741 | "amount_refunded": float64(0),
742 | "refund_status": nil,
743 | "captured": false,
744 | "description": nil,
745 | "card_id": nil,
746 | "bank": nil,
747 | "wallet": nil,
748 | "vpa": "failure@razorpay",
749 | "email": "[email protected]",
750 | "contact": "+919999999999",
751 | "notes": map[string]interface{}{
752 | "notes_key_1": "Tea, Earl Grey, Hot",
753 | "notes_key_2": "Tea, Earl Grey… decaf.",
754 | },
755 | "fee": nil,
756 | "tax": nil,
757 | "error_code": "BAD_REQUEST_ERROR",
758 | "error_description": "Payment was unsuccessful due to a temporary issue.",
759 | "error_source": "gateway",
760 | "error_step": "payment_response",
761 | "error_reason": "payment_failed",
762 | "acquirer_data": map[string]interface{}{
763 | "rrn": nil,
764 | },
765 | "created_at": float64(1701688684),
766 | "upi": map[string]interface{}{
767 | "vpa": "failure@razorpay",
768 | },
769 | },
770 | map[string]interface{}{
771 | "id": "pay_N8FVRD1DzYzBh1",
772 | "entity": "payment",
773 | "amount": float64(100),
774 | "currency": "INR",
775 | "status": "captured",
776 | "order_id": "order_N8FRN5zTm5S3wx",
777 | "invoice_id": nil,
778 | "international": false,
779 | "method": "upi",
780 | "amount_refunded": float64(0),
781 | "refund_status": nil,
782 | "captured": true,
783 | "description": nil,
784 | "card_id": nil,
785 | "bank": nil,
786 | "wallet": nil,
787 | "vpa": "success@razorpay",
788 | "email": "[email protected]",
789 | "contact": "+919999999999",
790 | "notes": map[string]interface{}{
791 | "notes_key_1": "Tea, Earl Grey, Hot",
792 | "notes_key_2": "Tea, Earl Grey… decaf.",
793 | },
794 | "fee": float64(2),
795 | "tax": float64(0),
796 | "error_code": nil,
797 | "error_description": nil,
798 | "error_source": nil,
799 | "error_step": nil,
800 | "error_reason": nil,
801 | "acquirer_data": map[string]interface{}{
802 | "rrn": "267567962619",
803 | "upi_transaction_id": "F5B66C7C07CA6FEAD77E956DC2FC7ABE",
804 | },
805 | "created_at": float64(1701688721),
806 | "upi": map[string]interface{}{
807 | "vpa": "success@razorpay",
808 | },
809 | },
810 | },
811 | }
812 |
813 | orderNotFoundResp := map[string]interface{}{
814 | "error": map[string]interface{}{
815 | "code": "BAD_REQUEST_ERROR",
816 | "description": "order not found",
817 | },
818 | }
819 |
820 | tests := []RazorpayToolTestCase{
821 | {
822 | Name: "successful fetch of order payments",
823 | Request: map[string]interface{}{
824 | "order_id": "order_N8FRN5zTm5S3wx",
825 | },
826 | MockHttpClient: func() (*http.Client, *httptest.Server) {
827 | return mock.NewHTTPClient(
828 | mock.Endpoint{
829 | Path: fmt.Sprintf(
830 | fetchOrderPaymentsPathFmt,
831 | "order_N8FRN5zTm5S3wx",
832 | ),
833 | Method: "GET",
834 | Response: paymentsResp,
835 | },
836 | )
837 | },
838 | ExpectError: false,
839 | ExpectedResult: paymentsResp,
840 | },
841 | {
842 | Name: "order not found",
843 | Request: map[string]interface{}{
844 | "order_id": "order_invalid",
845 | },
846 | MockHttpClient: func() (*http.Client, *httptest.Server) {
847 | return mock.NewHTTPClient(
848 | mock.Endpoint{
849 | Path: fmt.Sprintf(
850 | fetchOrderPaymentsPathFmt,
851 | "order_invalid",
852 | ),
853 | Method: "GET",
854 | Response: orderNotFoundResp,
855 | },
856 | )
857 | },
858 | ExpectError: true,
859 | ExpectedErrMsg: "fetching payments for order failed: order not found",
860 | },
861 | {
862 | Name: "missing order_id parameter",
863 | Request: map[string]interface{}{},
864 | MockHttpClient: nil, // No HTTP client needed for validation error
865 | ExpectError: true,
866 | ExpectedErrMsg: "missing required parameter: order_id",
867 | },
868 | }
869 |
870 | for _, tc := range tests {
871 | t.Run(tc.Name, func(t *testing.T) {
872 | runToolTest(t, tc, FetchOrderPayments, "Order")
873 | })
874 | }
875 | }
876 |
877 | func Test_UpdateOrder(t *testing.T) {
878 | updateOrderPathFmt := fmt.Sprintf(
879 | "/%s%s/%%s",
880 | constants.VERSION_V1,
881 | constants.ORDER_URL,
882 | )
883 |
884 | updatedOrderResp := map[string]interface{}{
885 | "id": "order_EKwxwAgItmmXdp",
886 | "entity": "order",
887 | "amount": float64(10000),
888 | "currency": "INR",
889 | "receipt": "receipt-123",
890 | "status": "created",
891 | "attempts": float64(0),
892 | "created_at": float64(1572505143),
893 | "notes": map[string]interface{}{
894 | "customer_name": "updated-customer",
895 | "product_name": "updated-product",
896 | },
897 | }
898 |
899 | orderNotFoundResp := map[string]interface{}{
900 | "error": map[string]interface{}{
901 | "code": "BAD_REQUEST_ERROR",
902 | "description": "order not found",
903 | },
904 | }
905 |
906 | tests := []RazorpayToolTestCase{
907 | {
908 | Name: "successful order update",
909 | Request: map[string]interface{}{
910 | "order_id": "order_EKwxwAgItmmXdp",
911 | "notes": map[string]interface{}{
912 | "customer_name": "updated-customer",
913 | "product_name": "updated-product",
914 | },
915 | },
916 | MockHttpClient: func() (*http.Client, *httptest.Server) {
917 | return mock.NewHTTPClient(
918 | mock.Endpoint{
919 | Path: fmt.Sprintf(
920 | updateOrderPathFmt, "order_EKwxwAgItmmXdp"),
921 | Method: "PATCH",
922 | Response: updatedOrderResp,
923 | },
924 | )
925 | },
926 | ExpectError: false,
927 | ExpectedResult: updatedOrderResp,
928 | },
929 | {
930 | Name: "missing required parameters - order_id",
931 | Request: map[string]interface{}{
932 | // Missing order_id
933 | "notes": map[string]interface{}{
934 | "customer_name": "updated-customer",
935 | "product_name": "updated-product",
936 | },
937 | },
938 | MockHttpClient: nil, // No HTTP client needed for validation error
939 | ExpectError: true,
940 | ExpectedErrMsg: "missing required parameter: order_id",
941 | },
942 | {
943 | Name: "missing required parameters - notes",
944 | Request: map[string]interface{}{
945 | "order_id": "order_EKwxwAgItmmXdp",
946 | // Missing notes
947 | },
948 | MockHttpClient: nil, // No HTTP client needed for validation error
949 | ExpectError: true,
950 | ExpectedErrMsg: "missing required parameter: notes",
951 | },
952 | {
953 | Name: "order not found",
954 | Request: map[string]interface{}{
955 | "order_id": "order_invalid_id",
956 | "notes": map[string]interface{}{
957 | "customer_name": "updated-customer",
958 | "product_name": "updated-product",
959 | },
960 | },
961 | MockHttpClient: func() (*http.Client, *httptest.Server) {
962 | return mock.NewHTTPClient(
963 | mock.Endpoint{
964 | Path: fmt.Sprintf(updateOrderPathFmt, "order_invalid_id"),
965 | Method: "PATCH",
966 | Response: orderNotFoundResp,
967 | },
968 | )
969 | },
970 | ExpectError: true,
971 | ExpectedErrMsg: "updating order failed: order not found",
972 | },
973 | }
974 |
975 | for _, tc := range tests {
976 | t.Run(tc.Name, func(t *testing.T) {
977 | runToolTest(t, tc, UpdateOrder, "Order")
978 | })
979 | }
980 | }
981 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/payments.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 | "time"
10 |
11 | rzpsdk "github.com/razorpay/razorpay-go"
12 |
13 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
14 | "github.com/razorpay/razorpay-mcp-server/pkg/observability"
15 | )
16 |
17 | // FetchPayment returns a tool that fetches payment details using payment_id
18 | func FetchPayment(
19 | obs *observability.Observability,
20 | client *rzpsdk.Client,
21 | ) mcpgo.Tool {
22 | parameters := []mcpgo.ToolParameter{
23 | mcpgo.WithString(
24 | "payment_id",
25 | mcpgo.Description("payment_id is unique identifier "+
26 | "of the payment to be retrieved."),
27 | mcpgo.Required(),
28 | ),
29 | }
30 |
31 | handler := func(
32 | ctx context.Context,
33 | r mcpgo.CallToolRequest,
34 | ) (*mcpgo.ToolResult, error) {
35 | // Get client from context or use default
36 | client, err := getClientFromContextOrDefault(ctx, client)
37 | if err != nil {
38 | return mcpgo.NewToolResultError(err.Error()), nil
39 | }
40 |
41 | params := make(map[string]interface{})
42 |
43 | validator := NewValidator(&r).
44 | ValidateAndAddRequiredString(params, "payment_id")
45 |
46 | if result, err := validator.HandleErrorsIfAny(); result != nil {
47 | return result, err
48 | }
49 |
50 | paymentId := params["payment_id"].(string)
51 |
52 | payment, err := client.Payment.Fetch(paymentId, nil, nil)
53 | if err != nil {
54 | return mcpgo.NewToolResultError(
55 | fmt.Sprintf("fetching payment failed: %s", err.Error())), nil
56 | }
57 |
58 | return mcpgo.NewToolResultJSON(payment)
59 | }
60 |
61 | return mcpgo.NewTool(
62 | "fetch_payment",
63 | "Use this tool to retrieve the details of a specific payment "+
64 | "using its id. Amount returned is in paisa",
65 | parameters,
66 | handler,
67 | )
68 | }
69 |
70 | // FetchPaymentCardDetails returns a tool that fetches card details
71 | // for a payment
72 | func FetchPaymentCardDetails(
73 | obs *observability.Observability,
74 | client *rzpsdk.Client,
75 | ) mcpgo.Tool {
76 | parameters := []mcpgo.ToolParameter{
77 | mcpgo.WithString(
78 | "payment_id",
79 | mcpgo.Description("Unique identifier of the payment for which "+
80 | "you want to retrieve card details. Must start with 'pay_'"),
81 | mcpgo.Required(),
82 | ),
83 | }
84 |
85 | handler := func(
86 | ctx context.Context,
87 | r mcpgo.CallToolRequest,
88 | ) (*mcpgo.ToolResult, error) {
89 | // Get client from context or use default
90 | client, err := getClientFromContextOrDefault(ctx, client)
91 | if err != nil {
92 | return mcpgo.NewToolResultError(err.Error()), nil
93 | }
94 |
95 | params := make(map[string]interface{})
96 |
97 | validator := NewValidator(&r).
98 | ValidateAndAddRequiredString(params, "payment_id")
99 |
100 | if result, err := validator.HandleErrorsIfAny(); result != nil {
101 | return result, err
102 | }
103 |
104 | paymentId := params["payment_id"].(string)
105 |
106 | cardDetails, err := client.Payment.FetchCardDetails(
107 | paymentId, nil, nil)
108 |
109 | if err != nil {
110 | return mcpgo.NewToolResultError(
111 | fmt.Sprintf("fetching card details failed: %s", err.Error())), nil
112 | }
113 |
114 | return mcpgo.NewToolResultJSON(cardDetails)
115 | }
116 |
117 | return mcpgo.NewTool(
118 | "fetch_payment_card_details",
119 | "Use this tool to retrieve the details of the card used to make a payment. "+
120 | "Only works for payments made using a card.",
121 | parameters,
122 | handler,
123 | )
124 | }
125 |
126 | // UpdatePayment returns a tool that updates the notes for a payment
127 | func UpdatePayment(
128 | obs *observability.Observability,
129 | client *rzpsdk.Client,
130 | ) mcpgo.Tool {
131 | parameters := []mcpgo.ToolParameter{
132 | mcpgo.WithString(
133 | "payment_id",
134 | mcpgo.Description("Unique identifier of the payment to be updated. "+
135 | "Must start with 'pay_'"),
136 | mcpgo.Required(),
137 | ),
138 | mcpgo.WithObject(
139 | "notes",
140 | mcpgo.Description("Key-value pairs that can be used to store additional "+
141 | "information about the payment. Values must be strings or integers."),
142 | mcpgo.Required(),
143 | ),
144 | }
145 |
146 | handler := func(
147 | ctx context.Context,
148 | r mcpgo.CallToolRequest,
149 | ) (*mcpgo.ToolResult, error) {
150 | // Get client from context or use default
151 | client, err := getClientFromContextOrDefault(ctx, client)
152 | if err != nil {
153 | return mcpgo.NewToolResultError(err.Error()), nil
154 | }
155 |
156 | params := make(map[string]interface{})
157 | paymentUpdateReq := make(map[string]interface{})
158 |
159 | validator := NewValidator(&r).
160 | ValidateAndAddRequiredString(params, "payment_id").
161 | ValidateAndAddRequiredMap(paymentUpdateReq, "notes")
162 |
163 | if result, err := validator.HandleErrorsIfAny(); result != nil {
164 | return result, err
165 | }
166 |
167 | paymentId := params["payment_id"].(string)
168 |
169 | // Update the payment
170 | updatedPayment, err := client.Payment.Edit(paymentId, paymentUpdateReq, nil)
171 | if err != nil {
172 | return mcpgo.NewToolResultError(
173 | fmt.Sprintf("updating payment failed: %s", err.Error())), nil
174 | }
175 |
176 | return mcpgo.NewToolResultJSON(updatedPayment)
177 | }
178 |
179 | return mcpgo.NewTool(
180 | "update_payment",
181 | "Use this tool to update the notes field of a payment. Notes are "+
182 | "key-value pairs that can be used to store additional information.", //nolint:lll
183 | parameters,
184 | handler,
185 | )
186 | }
187 |
188 | // CapturePayment returns a tool that captures an authorized payment
189 | func CapturePayment(
190 | obs *observability.Observability,
191 | client *rzpsdk.Client,
192 | ) mcpgo.Tool {
193 | parameters := []mcpgo.ToolParameter{
194 | mcpgo.WithString(
195 | "payment_id",
196 | mcpgo.Description("Unique identifier of the payment to be captured. Should start with 'pay_'"), //nolint:lll
197 | mcpgo.Required(),
198 | ),
199 | mcpgo.WithNumber(
200 | "amount",
201 | mcpgo.Description("The amount to be captured (in paisa). "+
202 | "Should be equal to the authorized amount"),
203 | mcpgo.Required(),
204 | ),
205 | mcpgo.WithString(
206 | "currency",
207 | mcpgo.Description("ISO code of the currency in which the payment "+
208 | "was made (e.g., INR)"),
209 | mcpgo.Required(),
210 | ),
211 | }
212 |
213 | handler := func(
214 | ctx context.Context,
215 | r mcpgo.CallToolRequest,
216 | ) (*mcpgo.ToolResult, error) {
217 | // Get client from context or use default
218 | client, err := getClientFromContextOrDefault(ctx, client)
219 | if err != nil {
220 | return mcpgo.NewToolResultError(err.Error()), nil
221 | }
222 |
223 | params := make(map[string]interface{})
224 | paymentCaptureReq := make(map[string]interface{})
225 |
226 | validator := NewValidator(&r).
227 | ValidateAndAddRequiredString(params, "payment_id").
228 | ValidateAndAddRequiredInt(params, "amount").
229 | ValidateAndAddRequiredString(paymentCaptureReq, "currency")
230 |
231 | if result, err := validator.HandleErrorsIfAny(); result != nil {
232 | return result, err
233 | }
234 |
235 | paymentId := params["payment_id"].(string)
236 | amount := int(params["amount"].(int64))
237 |
238 | // Capture the payment
239 | payment, err := client.Payment.Capture(
240 | paymentId,
241 | amount,
242 | paymentCaptureReq,
243 | nil,
244 | )
245 | if err != nil {
246 | return mcpgo.NewToolResultError(
247 | fmt.Sprintf("capturing payment failed: %s", err.Error())), nil
248 | }
249 |
250 | return mcpgo.NewToolResultJSON(payment)
251 | }
252 |
253 | return mcpgo.NewTool(
254 | "capture_payment",
255 | "Use this tool to capture a previously authorized payment. Only payments with 'authorized' status can be captured", //nolint:lll
256 | parameters,
257 | handler,
258 | )
259 | }
260 |
261 | // FetchAllPayments returns a tool to fetch multiple payments with filtering and pagination
262 | //
263 | //nolint:lll
264 | func FetchAllPayments(
265 | obs *observability.Observability,
266 | client *rzpsdk.Client,
267 | ) mcpgo.Tool {
268 | parameters := []mcpgo.ToolParameter{
269 | // Pagination parameters
270 | mcpgo.WithNumber(
271 | "count",
272 | mcpgo.Description("Number of payments to fetch "+
273 | "(default: 10, max: 100)"),
274 | mcpgo.Min(1),
275 | mcpgo.Max(100),
276 | ),
277 | mcpgo.WithNumber(
278 | "skip",
279 | mcpgo.Description("Number of payments to skip (default: 0)"),
280 | mcpgo.Min(0),
281 | ),
282 | // Time range filters
283 | mcpgo.WithNumber(
284 | "from",
285 | mcpgo.Description("Unix timestamp (in seconds) from when "+
286 | "payments are to be fetched"),
287 | mcpgo.Min(0),
288 | ),
289 | mcpgo.WithNumber(
290 | "to",
291 | mcpgo.Description("Unix timestamp (in seconds) up till when "+
292 | "payments are to be fetched"),
293 | mcpgo.Min(0),
294 | ),
295 | }
296 |
297 | handler := func(
298 | ctx context.Context,
299 | r mcpgo.CallToolRequest,
300 | ) (*mcpgo.ToolResult, error) {
301 | // Get client from context or use default
302 | client, err := getClientFromContextOrDefault(ctx, client)
303 | if err != nil {
304 | return mcpgo.NewToolResultError(err.Error()), nil
305 | }
306 |
307 | // Create query parameters map
308 | paymentListOptions := make(map[string]interface{})
309 |
310 | validator := NewValidator(&r).
311 | ValidateAndAddPagination(paymentListOptions).
312 | ValidateAndAddOptionalInt(paymentListOptions, "from").
313 | ValidateAndAddOptionalInt(paymentListOptions, "to")
314 |
315 | if result, err := validator.HandleErrorsIfAny(); result != nil {
316 | return result, err
317 | }
318 |
319 | // Fetch all payments using Razorpay SDK
320 | payments, err := client.Payment.All(paymentListOptions, nil)
321 | if err != nil {
322 | return mcpgo.NewToolResultError(
323 | fmt.Sprintf("fetching payments failed: %s", err.Error())), nil
324 | }
325 |
326 | return mcpgo.NewToolResultJSON(payments)
327 | }
328 |
329 | return mcpgo.NewTool(
330 | "fetch_all_payments",
331 | "Fetch all payments with optional filtering and pagination",
332 | parameters,
333 | handler,
334 | )
335 | }
336 |
337 | // extractPaymentID extracts the payment ID from the payment response
338 | func extractPaymentID(payment map[string]interface{}) string {
339 | if id, exists := payment["razorpay_payment_id"]; exists && id != nil {
340 | return id.(string)
341 | }
342 | return ""
343 | }
344 |
345 | // extractNextActions extracts all available actions from the payment response
346 | func extractNextActions(
347 | payment map[string]interface{},
348 | ) []map[string]interface{} {
349 | var actions []map[string]interface{}
350 | if nextArray, exists := payment["next"]; exists && nextArray != nil {
351 | if nextSlice, ok := nextArray.([]interface{}); ok {
352 | for _, item := range nextSlice {
353 | if nextItem, ok := item.(map[string]interface{}); ok {
354 | actions = append(actions, nextItem)
355 | }
356 | }
357 | }
358 | }
359 | return actions
360 | }
361 |
362 | // OTPResponse represents the response from OTP generation API
363 |
364 | // sendOtp sends an OTP to the customer and returns the response
365 | func sendOtp(otpUrl string) error {
366 | if otpUrl == "" {
367 | return fmt.Errorf("OTP URL is empty")
368 | }
369 | // Validate URL is safe and from Razorpay domain for security
370 | parsedURL, err := url.Parse(otpUrl)
371 | if err != nil {
372 | return fmt.Errorf("invalid OTP URL: %s", err.Error())
373 | }
374 |
375 | if parsedURL.Scheme != "https" {
376 | return fmt.Errorf("OTP URL must use HTTPS")
377 | }
378 |
379 | if !strings.Contains(parsedURL.Host, "razorpay.com") {
380 | return fmt.Errorf("OTP URL must be from Razorpay domain")
381 | }
382 |
383 | // Create a secure HTTP client with timeout
384 | client := &http.Client{
385 | Timeout: 10 * time.Second,
386 | }
387 |
388 | req, err := http.NewRequest("POST", otpUrl, nil)
389 | if err != nil {
390 | return fmt.Errorf("failed to create OTP request: %s", err.Error())
391 | }
392 | req.Header.Set("Content-Type", "application/json")
393 |
394 | resp, err := client.Do(req)
395 | if err != nil {
396 | return fmt.Errorf("OTP generation failed: %s", err.Error())
397 | }
398 | defer resp.Body.Close()
399 |
400 | // Validate HTTP response status
401 | if resp.StatusCode < 200 || resp.StatusCode >= 300 {
402 | return fmt.Errorf("OTP generation failed with HTTP status: %d",
403 | resp.StatusCode)
404 | }
405 | return nil
406 | }
407 |
408 | // buildInitiatePaymentResponse constructs the response for initiate payment
409 | func buildInitiatePaymentResponse(
410 | payment map[string]interface{},
411 | paymentID string,
412 | actions []map[string]interface{},
413 | ) (map[string]interface{}, string) {
414 | response := map[string]interface{}{
415 | "razorpay_payment_id": paymentID,
416 | "payment_details": payment,
417 | "status": "payment_initiated",
418 | "message": "Payment initiated successfully using " +
419 | "S2S JSON v1 flow",
420 | }
421 | otpUrl := ""
422 |
423 | if len(actions) > 0 {
424 | response["available_actions"] = actions
425 |
426 | // Add guidance based on available actions
427 | var actionTypes []string
428 | hasOTP := false
429 | hasRedirect := false
430 | hasUPICollect := false
431 | hasUPIIntent := false
432 |
433 | for _, action := range actions {
434 | if actionType, exists := action["action"]; exists {
435 | actionStr := actionType.(string)
436 | actionTypes = append(actionTypes, actionStr)
437 | if actionStr == "otp_generate" {
438 | hasOTP = true
439 | otpUrl = action["url"].(string)
440 | }
441 |
442 | if actionStr == "redirect" {
443 | hasRedirect = true
444 | }
445 |
446 | if actionStr == "upi_collect" {
447 | hasUPICollect = true
448 | }
449 |
450 | if actionStr == "upi_intent" {
451 | hasUPIIntent = true
452 | }
453 | }
454 | }
455 |
456 | switch {
457 | case hasOTP:
458 | response["message"] = "Payment initiated. OTP authentication is " +
459 | "available. " +
460 | "Use the 'submit_otp' tool to submit OTP received by the customer " +
461 | "for authentication."
462 | addNextStepInstructions(response, paymentID)
463 | case hasRedirect:
464 | response["message"] = "Payment initiated. Redirect authentication is " +
465 | "available. Use the redirect URL provided in available_actions."
466 | case hasUPICollect:
467 | response["message"] = fmt.Sprintf(
468 | "Payment initiated. Available actions: %v", actionTypes)
469 | case hasUPIIntent:
470 | response["message"] = fmt.Sprintf(
471 | "Payment initiated. Available actions: %v", actionTypes)
472 | default:
473 | response["message"] = fmt.Sprintf(
474 | "Payment initiated. Available actions: %v", actionTypes)
475 | }
476 | } else {
477 | addFallbackNextStepInstructions(response, paymentID)
478 | }
479 |
480 | return response, otpUrl
481 | }
482 |
483 | // addNextStepInstructions adds next step guidance to the response
484 | func addNextStepInstructions(
485 | response map[string]interface{},
486 | paymentID string,
487 | ) {
488 | if paymentID != "" {
489 | response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
490 | "'submit_otp' to proceed to enter OTP."
491 | response["next_tool"] = "resend_otp"
492 | response["next_tool_params"] = map[string]interface{}{
493 | "payment_id": paymentID,
494 | }
495 | }
496 | }
497 |
498 | // addFallbackNextStepInstructions adds fallback next step guidance
499 | func addFallbackNextStepInstructions(
500 | response map[string]interface{},
501 | paymentID string,
502 | ) {
503 | if paymentID != "" {
504 | response["next_step"] = "Use 'resend_otp' to regenerate OTP or " +
505 | "'submit_otp' to proceed to enter OTP if " +
506 | "OTP authentication is required."
507 | response["next_tool"] = "resend_otp"
508 | response["next_tool_params"] = map[string]interface{}{
509 | "payment_id": paymentID,
510 | }
511 | }
512 | }
513 |
514 | // addContactAndEmailToPaymentData adds contact and email to payment data
515 | func addContactAndEmailToPaymentData(
516 | paymentData map[string]interface{},
517 | params map[string]interface{},
518 | ) {
519 | // Add contact if provided
520 | if contact, exists := params["contact"]; exists && contact != "" {
521 | paymentData["contact"] = contact
522 | }
523 |
524 | // Add email if provided, otherwise generate from contact
525 | if email, exists := params["email"]; exists && email != "" {
526 | paymentData["email"] = email
527 | } else if contact, exists := paymentData["contact"]; exists && contact != "" {
528 | paymentData["email"] = contact.(string) + "@mcp.razorpay.com"
529 | }
530 | }
531 |
532 | // addAdditionalPaymentParameters adds additional parameters for UPI collect
533 | // and other flows
534 | func addAdditionalPaymentParameters(
535 | paymentData map[string]interface{},
536 | params map[string]interface{},
537 | ) {
538 | // Note: customer_id is now handled explicitly in buildPaymentData
539 |
540 | // Add method if provided
541 | if method, exists := params["method"]; exists && method != "" {
542 | paymentData["method"] = method
543 | }
544 |
545 | // Add save if provided
546 | if save, exists := params["save"]; exists {
547 | paymentData["save"] = save
548 | }
549 |
550 | // Add recurring if provided
551 | if recurring, exists := params["recurring"]; exists {
552 | paymentData["recurring"] = recurring
553 | }
554 |
555 | // Add UPI parameters if provided
556 | if upiParams, exists := params["upi"]; exists && upiParams != nil {
557 | if upiMap, ok := upiParams.(map[string]interface{}); ok {
558 | paymentData["upi"] = upiMap
559 | }
560 | }
561 | }
562 |
563 | // processUPIParameters handles VPA and UPI intent parameter processing
564 | func processUPIParameters(params map[string]interface{}) {
565 | vpa, hasVPA := params["vpa"]
566 | upiIntent, hasUPIIntent := params["upi_intent"]
567 |
568 | // Handle VPA parameter (UPI collect flow)
569 | if hasVPA && vpa != "" {
570 | // Set method to UPI
571 | params["method"] = "upi"
572 | // Set UPI parameters for collect flow
573 | params["upi"] = map[string]interface{}{
574 | "flow": "collect",
575 | "expiry_time": "6",
576 | "vpa": vpa,
577 | }
578 | }
579 |
580 | // Handle UPI intent parameter (UPI intent flow)
581 | if hasUPIIntent && upiIntent == true {
582 | // Set method to UPI
583 | params["method"] = "upi"
584 | // Set UPI parameters for intent flow
585 | params["upi"] = map[string]interface{}{
586 | "flow": "intent",
587 | }
588 | }
589 | }
590 |
591 | // createOrGetCustomer creates or gets a customer if contact is provided
592 | func createOrGetCustomer(
593 | client *rzpsdk.Client,
594 | params map[string]interface{},
595 | ) (map[string]interface{}, error) {
596 | contactValue, exists := params["contact"]
597 | if !exists || contactValue == "" {
598 | return nil, nil
599 | }
600 |
601 | contact := contactValue.(string)
602 | customerData := map[string]interface{}{
603 | "contact": contact,
604 | "fail_existing": "0", // Get existing customer if exists
605 | }
606 |
607 | // Create/get customer using Razorpay SDK
608 | customer, err := client.Customer.Create(customerData, nil)
609 | if err != nil {
610 | return nil, fmt.Errorf(
611 | "failed to create/fetch customer with contact %s: %v",
612 | contact,
613 | err,
614 | )
615 | }
616 | return customer, nil
617 | }
618 |
619 | // buildPaymentData constructs the payment data for the API call
620 | func buildPaymentData(
621 | params map[string]interface{},
622 | currency string,
623 | customerId string,
624 | ) *map[string]interface{} {
625 | paymentData := map[string]interface{}{
626 | "amount": params["amount"],
627 | "currency": currency,
628 | "order_id": params["order_id"],
629 | }
630 | if customerId != "" {
631 | paymentData["customer_id"] = customerId
632 | }
633 |
634 | // Add token if provided (required for saved payment methods,
635 | // optional for UPI collect)
636 | if token, exists := params["token"]; exists && token != "" {
637 | paymentData["token"] = token
638 | }
639 |
640 | // Add contact and email parameters
641 | addContactAndEmailToPaymentData(paymentData, params)
642 |
643 | // Add additional parameters for UPI collect and other flows
644 | addAdditionalPaymentParameters(paymentData, params)
645 |
646 | // Add force_terminal_id if provided (for single block multiple debit orders)
647 | if terminalID, exists := params["force_terminal_id"]; exists &&
648 | terminalID != "" {
649 | paymentData["force_terminal_id"] = terminalID
650 | }
651 |
652 | return &paymentData
653 | }
654 |
655 | // processPaymentResult processes the payment creation result
656 | func processPaymentResult(
657 | payment map[string]interface{},
658 | ) (map[string]interface{}, error) {
659 | // Extract payment ID and next actions from the response
660 | paymentID := extractPaymentID(payment)
661 | actions := extractNextActions(payment)
662 |
663 | // Build structured response using the helper function
664 | response, otpUrl := buildInitiatePaymentResponse(payment, paymentID, actions)
665 |
666 | // Only send OTP if there's an OTP URL
667 | if otpUrl != "" {
668 | err := sendOtp(otpUrl)
669 | if err != nil {
670 | return nil, fmt.Errorf("OTP generation failed: %s", err.Error())
671 | }
672 | }
673 |
674 | return response, nil
675 | }
676 |
677 | // InitiatePayment returns a tool that initiates a payment using order_id
678 | // and token
679 | // This implements the S2S JSON v1 flow for creating payments
680 | func InitiatePayment(
681 | obs *observability.Observability,
682 | client *rzpsdk.Client,
683 | ) mcpgo.Tool {
684 | parameters := []mcpgo.ToolParameter{
685 | mcpgo.WithNumber(
686 | "amount",
687 | mcpgo.Description("Payment amount in the smallest currency sub-unit "+
688 | "(e.g., for ₹100, use 10000)"),
689 | mcpgo.Required(),
690 | mcpgo.Min(100),
691 | ),
692 | mcpgo.WithString(
693 | "currency",
694 | mcpgo.Description("Currency code for the payment. Default is 'INR'"),
695 | ),
696 | mcpgo.WithString(
697 | "token",
698 | mcpgo.Description("Token ID of the saved payment method. "+
699 | "Must start with 'token_'"),
700 | ),
701 | mcpgo.WithString(
702 | "order_id",
703 | mcpgo.Description("Order ID for which the payment is being initiated. "+
704 | "Must start with 'order_'"),
705 | mcpgo.Required(),
706 | ),
707 | mcpgo.WithString(
708 | "email",
709 | mcpgo.Description("Customer's email address (optional)"),
710 | ),
711 | mcpgo.WithString(
712 | "contact",
713 | mcpgo.Description("Customer's phone number"),
714 | ),
715 | mcpgo.WithString(
716 | "customer_id",
717 | mcpgo.Description("Customer ID for the payment. "+
718 | "Must start with 'cust_'"),
719 | ),
720 | mcpgo.WithBoolean(
721 | "save",
722 | mcpgo.Description("Whether to save the payment method for future use"),
723 | ),
724 | mcpgo.WithString(
725 | "vpa",
726 | mcpgo.Description("Virtual Payment Address (VPA) for UPI payment. "+
727 | "When provided, automatically sets method='upi' and UPI parameters "+
728 | "with flow='collect' and expiry_time='6' (e.g., '9876543210@ptsbi')"),
729 | ),
730 | mcpgo.WithBoolean(
731 | "upi_intent",
732 | mcpgo.Description("Enable UPI intent flow. "+
733 | "When set to true, automatically sets method='upi' and UPI parameters "+
734 | "with flow='intent'. The API will return a UPI URL in the response."),
735 | ),
736 | mcpgo.WithBoolean(
737 | "recurring",
738 | mcpgo.Description("Set this to true for recurring payments like "+
739 | "single block multiple debit."),
740 | ),
741 | mcpgo.WithString(
742 | "force_terminal_id",
743 | mcpgo.Description("Terminal ID to be passed in case of single block "+
744 | "multiple debit order."),
745 | ),
746 | }
747 |
748 | handler := func(
749 | ctx context.Context,
750 | r mcpgo.CallToolRequest,
751 | ) (*mcpgo.ToolResult, error) {
752 | // Get client from context or use default
753 | client, err := getClientFromContextOrDefault(ctx, client)
754 | if err != nil {
755 | return mcpgo.NewToolResultError(err.Error()), nil
756 | }
757 |
758 | params := make(map[string]interface{})
759 |
760 | validator := NewValidator(&r).
761 | ValidateAndAddRequiredInt(params, "amount").
762 | ValidateAndAddOptionalString(params, "currency").
763 | ValidateAndAddOptionalString(params, "token").
764 | ValidateAndAddRequiredString(params, "order_id").
765 | ValidateAndAddOptionalString(params, "email").
766 | ValidateAndAddOptionalString(params, "contact").
767 | ValidateAndAddOptionalString(params, "customer_id").
768 | ValidateAndAddOptionalBool(params, "save").
769 | ValidateAndAddOptionalString(params, "vpa").
770 | ValidateAndAddOptionalBool(params, "upi_intent").
771 | ValidateAndAddOptionalBool(params, "recurring").
772 | ValidateAndAddOptionalString(params, "force_terminal_id")
773 |
774 | if result, err := validator.HandleErrorsIfAny(); result != nil {
775 | return result, err
776 | }
777 |
778 | // Set default currency
779 | currency := "INR"
780 | if c, exists := params["currency"]; exists && c != "" {
781 | currency = c.(string)
782 | }
783 |
784 | // Process UPI parameters (VPA for collect flow, upi_intent for intent flow)
785 | processUPIParameters(params)
786 |
787 | // Handle customer ID
788 | var customerID string
789 | if custID, exists := params["customer_id"]; exists && custID != "" {
790 | customerID = custID.(string)
791 | } else {
792 | // Create or get customer if contact is provided
793 | customer, err := createOrGetCustomer(client, params)
794 | if err != nil {
795 | return mcpgo.NewToolResultError(err.Error()), nil
796 | }
797 | if customer != nil {
798 | if id, ok := customer["id"].(string); ok {
799 | customerID = id
800 | }
801 | }
802 | }
803 |
804 | // Build payment data
805 | paymentDataPtr := buildPaymentData(params, currency, customerID)
806 | paymentData := *paymentDataPtr
807 |
808 | // Create payment using Razorpay SDK's CreatePaymentJson method
809 | // This follows the S2S JSON v1 flow:
810 | // https://api.razorpay.com/v1/payments/create/json
811 | payment, err := client.Payment.CreatePaymentJson(paymentData, nil)
812 | if err != nil {
813 | return mcpgo.NewToolResultError(
814 | fmt.Sprintf("initiating payment failed: %s", err.Error())), nil
815 | }
816 |
817 | // Process payment result
818 | response, err := processPaymentResult(payment)
819 | if err != nil {
820 | return mcpgo.NewToolResultError(err.Error()), nil
821 | }
822 |
823 | return mcpgo.NewToolResultJSON(response)
824 | }
825 |
826 | return mcpgo.NewTool(
827 | "initiate_payment",
828 | "Initiate a payment using the S2S JSON v1 flow. "+
829 | "Required parameters: amount and order_id. "+
830 | "For saved payment methods, provide token. "+
831 | "For UPI collect flow, provide 'vpa' parameter "+
832 | "which automatically sets UPI with flow='collect' and expiry_time='6'. "+
833 | "For UPI intent flow, set 'upi_intent=true' parameter "+
834 | "which automatically sets UPI with flow='intent' and API returns UPI URL. "+
835 | "Supports additional parameters like customer_id, email, "+
836 | "contact, save, and recurring. "+
837 | "Returns payment details including next action steps if required.",
838 | parameters,
839 | handler,
840 | )
841 | }
842 |
843 | // ResendOtp returns a tool that sends OTP for payment authentication
844 | func ResendOtp(
845 | obs *observability.Observability,
846 | client *rzpsdk.Client,
847 | ) mcpgo.Tool {
848 | parameters := []mcpgo.ToolParameter{
849 | mcpgo.WithString(
850 | "payment_id",
851 | mcpgo.Description("Unique identifier of the payment for which "+
852 | "OTP needs to be generated. Must start with 'pay_'"),
853 | mcpgo.Required(),
854 | ),
855 | }
856 |
857 | handler := func(
858 | ctx context.Context,
859 | r mcpgo.CallToolRequest,
860 | ) (*mcpgo.ToolResult, error) {
861 |
862 | // Get client from context or use default
863 | client, err := getClientFromContextOrDefault(ctx, client)
864 | if err != nil {
865 | return mcpgo.NewToolResultError(err.Error()), nil
866 | }
867 |
868 | params := make(map[string]interface{})
869 |
870 | validator := NewValidator(&r).
871 | ValidateAndAddRequiredString(params, "payment_id")
872 |
873 | if result, err := validator.HandleErrorsIfAny(); result != nil {
874 | return result, err
875 | }
876 |
877 | paymentID := params["payment_id"].(string)
878 |
879 | // Resend OTP using Razorpay SDK
880 | otpResponse, err := client.Payment.OtpResend(paymentID, nil, nil)
881 | if err != nil {
882 | return mcpgo.NewToolResultError(
883 | fmt.Sprintf("OTP resend failed: %s", err.Error())), nil
884 | }
885 |
886 | // Extract OTP submit URL from response
887 | otpSubmitURL := extractOtpSubmitURL(otpResponse)
888 |
889 | // Prepare response
890 | response := map[string]interface{}{
891 | "payment_id": paymentID,
892 | "status": "success",
893 | "message": "OTP sent successfully. Please enter the OTP received on your " +
894 | "mobile number to complete the payment.",
895 | "response_data": otpResponse,
896 | }
897 |
898 | // Add next step instructions if OTP submit URL is available
899 | if otpSubmitURL != "" {
900 | response["otp_submit_url"] = otpSubmitURL
901 | response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
902 | "from user to complete payment authentication."
903 | response["next_tool"] = "submit_otp"
904 | response["next_tool_params"] = map[string]interface{}{
905 | "payment_id": paymentID,
906 | "otp_string": "{OTP_CODE_FROM_USER}",
907 | }
908 | } else {
909 | response["next_step"] = "Use 'submit_otp' tool with the OTP code received " +
910 | "from user to complete payment authentication."
911 | response["next_tool"] = "submit_otp"
912 | response["next_tool_params"] = map[string]interface{}{
913 | "payment_id": paymentID,
914 | "otp_string": "{OTP_CODE_FROM_USER}",
915 | }
916 | }
917 |
918 | result, err := mcpgo.NewToolResultJSON(response)
919 | if err != nil {
920 | return mcpgo.NewToolResultError(
921 | fmt.Sprintf("JSON marshal error: %v", err)), nil
922 | }
923 | return result, nil
924 | }
925 |
926 | return mcpgo.NewTool(
927 | "resend_otp",
928 | "Resend OTP to the customer's registered mobile number if the previous "+
929 | "OTP was not received or has expired.",
930 | parameters,
931 | handler,
932 | )
933 | }
934 |
935 | // SubmitOtp returns a tool that submits OTP for payment verification
936 | func SubmitOtp(
937 | obs *observability.Observability,
938 | client *rzpsdk.Client,
939 | ) mcpgo.Tool {
940 | parameters := []mcpgo.ToolParameter{
941 | mcpgo.WithString(
942 | "otp_string",
943 | mcpgo.Description("OTP string received from the user"),
944 | mcpgo.Required(),
945 | ),
946 | mcpgo.WithString(
947 | "payment_id",
948 | mcpgo.Description("Unique identifier of the payment for which "+
949 | "OTP needs to be submitted. Must start with 'pay_'"),
950 | mcpgo.Required(),
951 | ),
952 | }
953 |
954 | handler := func(
955 | ctx context.Context,
956 | r mcpgo.CallToolRequest,
957 | ) (*mcpgo.ToolResult, error) {
958 | // Get client from context or use default
959 | client, err := getClientFromContextOrDefault(ctx, client)
960 | if err != nil {
961 | return mcpgo.NewToolResultError(err.Error()), nil
962 | }
963 |
964 | params := make(map[string]interface{})
965 |
966 | validator := NewValidator(&r).
967 | ValidateAndAddRequiredString(params, "otp_string").
968 | ValidateAndAddRequiredString(params, "payment_id")
969 |
970 | if result, err := validator.HandleErrorsIfAny(); result != nil {
971 | return result, err
972 | }
973 |
974 | paymentID := params["payment_id"].(string)
975 | data := map[string]interface{}{
976 | "otp": params["otp_string"].(string),
977 | }
978 | otpResponse, err := client.Payment.OtpSubmit(paymentID, data, nil)
979 |
980 | if err != nil {
981 | return mcpgo.NewToolResultError(
982 | fmt.Sprintf("OTP verification failed: %s", err.Error())), nil
983 | }
984 |
985 | // Prepare response
986 | response := map[string]interface{}{
987 | "payment_id": paymentID,
988 | "status": "success",
989 | "message": "OTP verified successfully.",
990 | "response_data": otpResponse,
991 | }
992 | result, err := mcpgo.NewToolResultJSON(response)
993 | if err != nil {
994 | return mcpgo.NewToolResultError(
995 | fmt.Sprintf("JSON marshal error: %v", err)), nil
996 | }
997 | return result, nil
998 | }
999 |
1000 | return mcpgo.NewTool(
1001 | "submit_otp",
1002 | "Verify and submit the OTP received by the customer to complete "+
1003 | "the payment authentication process.",
1004 | parameters,
1005 | handler,
1006 | )
1007 | }
1008 |
1009 | // extractOtpSubmitURL extracts the OTP submit URL from the payment response
1010 | func extractOtpSubmitURL(responseData interface{}) string {
1011 | jsonData, ok := responseData.(map[string]interface{})
1012 | if !ok {
1013 | return ""
1014 | }
1015 |
1016 | nextArray, exists := jsonData["next"]
1017 | if !exists || nextArray == nil {
1018 | return ""
1019 | }
1020 |
1021 | nextSlice, ok := nextArray.([]interface{})
1022 | if !ok {
1023 | return ""
1024 | }
1025 |
1026 | for _, item := range nextSlice {
1027 | nextItem, ok := item.(map[string]interface{})
1028 | if !ok {
1029 | continue
1030 | }
1031 |
1032 | action, exists := nextItem["action"]
1033 | if !exists || action != "otp_submit" {
1034 | continue
1035 | }
1036 |
1037 | submitURL, exists := nextItem["url"]
1038 | if exists && submitURL != nil {
1039 | if urlStr, ok := submitURL.(string); ok {
1040 | return urlStr
1041 | }
1042 | }
1043 | }
1044 |
1045 | return ""
1046 | }
1047 |
```
--------------------------------------------------------------------------------
/pkg/razorpay/tools_params_test.go:
--------------------------------------------------------------------------------
```go
1 | package razorpay
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 |
8 | "github.com/razorpay/razorpay-mcp-server/pkg/mcpgo"
9 | )
10 |
11 | func TestValidator(t *testing.T) {
12 | tests := []struct {
13 | name string
14 | args map[string]interface{}
15 | paramName string
16 | validationFunc func(*Validator, map[string]interface{}, string) *Validator
17 | expectError bool
18 | expectValue interface{}
19 | expectKey string
20 | }{
21 | // String tests
22 | {
23 | name: "required string - valid",
24 | args: map[string]interface{}{"test_param": "test_value"},
25 | paramName: "test_param",
26 | validationFunc: (*Validator).ValidateAndAddRequiredString,
27 | expectError: false,
28 | expectValue: "test_value",
29 | expectKey: "test_param",
30 | },
31 | {
32 | name: "required string - missing",
33 | args: map[string]interface{}{},
34 | paramName: "test_param",
35 | validationFunc: (*Validator).ValidateAndAddRequiredString,
36 | expectError: true,
37 | expectValue: nil,
38 | expectKey: "test_param",
39 | },
40 | {
41 | name: "optional string - valid",
42 | args: map[string]interface{}{"test_param": "test_value"},
43 | paramName: "test_param",
44 | validationFunc: (*Validator).ValidateAndAddOptionalString,
45 | expectError: false,
46 | expectValue: "test_value",
47 | expectKey: "test_param",
48 | },
49 | {
50 | name: "optional string - empty",
51 | args: map[string]interface{}{"test_param": ""},
52 | paramName: "test_param",
53 | validationFunc: (*Validator).ValidateAndAddOptionalString,
54 | expectError: false,
55 | expectValue: "",
56 | expectKey: "test_param",
57 | },
58 |
59 | // Int tests
60 | {
61 | name: "required int - valid",
62 | args: map[string]interface{}{"test_param": float64(123)},
63 | paramName: "test_param",
64 | validationFunc: (*Validator).ValidateAndAddRequiredInt,
65 | expectError: false,
66 | expectValue: int64(123),
67 | expectKey: "test_param",
68 | },
69 | {
70 | name: "optional int - valid",
71 | args: map[string]interface{}{"test_param": float64(123)},
72 | paramName: "test_param",
73 | validationFunc: (*Validator).ValidateAndAddOptionalInt,
74 | expectError: false,
75 | expectValue: int64(123),
76 | expectKey: "test_param",
77 | },
78 | {
79 | name: "optional int - zero",
80 | args: map[string]interface{}{"test_param": float64(0)},
81 | paramName: "test_param",
82 | validationFunc: (*Validator).ValidateAndAddOptionalInt,
83 | expectError: false,
84 | expectValue: int64(0), // we expect the zero values as is
85 | expectKey: "test_param",
86 | },
87 |
88 | // Float tests
89 | {
90 | name: "required float - valid",
91 | args: map[string]interface{}{"test_param": float64(123.45)},
92 | paramName: "test_param",
93 | validationFunc: (*Validator).ValidateAndAddRequiredFloat,
94 | expectError: false,
95 | expectValue: float64(123.45),
96 | expectKey: "test_param",
97 | },
98 | {
99 | name: "optional float - valid",
100 | args: map[string]interface{}{"test_param": float64(123.45)},
101 | paramName: "test_param",
102 | validationFunc: (*Validator).ValidateAndAddOptionalFloat,
103 | expectError: false,
104 | expectValue: float64(123.45),
105 | expectKey: "test_param",
106 | },
107 | {
108 | name: "optional float - zero",
109 | args: map[string]interface{}{"test_param": float64(0)},
110 | paramName: "test_param",
111 | validationFunc: (*Validator).ValidateAndAddOptionalFloat,
112 | expectError: false,
113 | expectValue: float64(0),
114 | expectKey: "test_param",
115 | },
116 |
117 | // Bool tests
118 | {
119 | name: "required bool - true",
120 | args: map[string]interface{}{"test_param": true},
121 | paramName: "test_param",
122 | validationFunc: (*Validator).ValidateAndAddRequiredBool,
123 | expectError: false,
124 | expectValue: true,
125 | expectKey: "test_param",
126 | },
127 | {
128 | name: "required bool - false",
129 | args: map[string]interface{}{"test_param": false},
130 | paramName: "test_param",
131 | validationFunc: (*Validator).ValidateAndAddRequiredBool,
132 | expectError: false,
133 | expectValue: false,
134 | expectKey: "test_param",
135 | },
136 | {
137 | name: "optional bool - true",
138 | args: map[string]interface{}{"test_param": true},
139 | paramName: "test_param",
140 | validationFunc: (*Validator).ValidateAndAddOptionalBool,
141 | expectError: false,
142 | expectValue: true,
143 | expectKey: "test_param",
144 | },
145 | {
146 | name: "optional bool - false",
147 | args: map[string]interface{}{"test_param": false},
148 | paramName: "test_param",
149 | validationFunc: (*Validator).ValidateAndAddOptionalBool,
150 | expectError: false,
151 | expectValue: false,
152 | expectKey: "test_param",
153 | },
154 |
155 | // Map tests
156 | {
157 | name: "required map - valid",
158 | args: map[string]interface{}{
159 | "test_param": map[string]interface{}{"key": "value"},
160 | },
161 | paramName: "test_param",
162 | validationFunc: (*Validator).ValidateAndAddRequiredMap,
163 | expectError: false,
164 | expectValue: map[string]interface{}{"key": "value"},
165 | expectKey: "test_param",
166 | },
167 | {
168 | name: "optional map - valid",
169 | args: map[string]interface{}{
170 | "test_param": map[string]interface{}{"key": "value"},
171 | },
172 | paramName: "test_param",
173 | validationFunc: (*Validator).ValidateAndAddOptionalMap,
174 | expectError: false,
175 | expectValue: map[string]interface{}{"key": "value"},
176 | expectKey: "test_param",
177 | },
178 | {
179 | name: "optional map - empty",
180 | args: map[string]interface{}{
181 | "test_param": map[string]interface{}{},
182 | },
183 | paramName: "test_param",
184 | validationFunc: (*Validator).ValidateAndAddOptionalMap,
185 | expectError: false,
186 | expectValue: map[string]interface{}{},
187 | expectKey: "test_param",
188 | },
189 |
190 | // Array tests
191 | {
192 | name: "required array - valid",
193 | args: map[string]interface{}{
194 | "test_param": []interface{}{"value1", "value2"},
195 | },
196 | paramName: "test_param",
197 | validationFunc: (*Validator).ValidateAndAddRequiredArray,
198 | expectError: false,
199 | expectValue: []interface{}{"value1", "value2"},
200 | expectKey: "test_param",
201 | },
202 | {
203 | name: "optional array - valid",
204 | args: map[string]interface{}{
205 | "test_param": []interface{}{"value1", "value2"},
206 | },
207 | paramName: "test_param",
208 | validationFunc: (*Validator).ValidateAndAddOptionalArray,
209 | expectError: false,
210 | expectValue: []interface{}{"value1", "value2"},
211 | expectKey: "test_param",
212 | },
213 | {
214 | name: "optional array - empty",
215 | args: map[string]interface{}{"test_param": []interface{}{}},
216 | paramName: "test_param",
217 | validationFunc: (*Validator).ValidateAndAddOptionalArray,
218 | expectError: false,
219 | expectValue: []interface{}{},
220 | expectKey: "test_param",
221 | },
222 |
223 | // Invalid type tests
224 | {
225 | name: "required string - wrong type",
226 | args: map[string]interface{}{"test_param": 123},
227 | paramName: "test_param",
228 | validationFunc: (*Validator).ValidateAndAddRequiredString,
229 | expectError: true,
230 | expectValue: nil,
231 | expectKey: "test_param",
232 | },
233 | {
234 | name: "required int - wrong type",
235 | args: map[string]interface{}{"test_param": "not a number"},
236 | paramName: "test_param",
237 | validationFunc: (*Validator).ValidateAndAddRequiredInt,
238 | expectError: true,
239 | expectValue: nil,
240 | expectKey: "test_param",
241 | },
242 | }
243 |
244 | for _, tt := range tests {
245 | t.Run(tt.name, func(t *testing.T) {
246 | result := make(map[string]interface{})
247 | request := &mcpgo.CallToolRequest{
248 | Arguments: tt.args,
249 | }
250 | validator := NewValidator(request)
251 |
252 | tt.validationFunc(validator, result, tt.paramName)
253 |
254 | if tt.expectError {
255 | assert.True(t, validator.HasErrors(), "Expected validation error")
256 | } else {
257 | assert.False(t, validator.HasErrors(), "Did not expect validation error")
258 | assert.Equal(t,
259 | tt.expectValue,
260 | result[tt.expectKey],
261 | "Parameter value mismatch",
262 | )
263 | }
264 | })
265 | }
266 | }
267 |
268 | func TestValidatorPagination(t *testing.T) {
269 | tests := []struct {
270 | name string
271 | args map[string]interface{}
272 | expectCount interface{}
273 | expectSkip interface{}
274 | expectError bool
275 | }{
276 | {
277 | name: "valid pagination params",
278 | args: map[string]interface{}{
279 | "count": float64(10),
280 | "skip": float64(5),
281 | },
282 | expectCount: int64(10),
283 | expectSkip: int64(5),
284 | expectError: false,
285 | },
286 | {
287 | name: "zero pagination params",
288 | args: map[string]interface{}{"count": float64(0), "skip": float64(0)},
289 | expectCount: int64(0),
290 | expectSkip: int64(0),
291 | expectError: false,
292 | },
293 | {
294 | name: "invalid count type",
295 | args: map[string]interface{}{
296 | "count": "not a number",
297 | "skip": float64(5),
298 | },
299 | expectCount: nil,
300 | expectSkip: int64(5),
301 | expectError: true,
302 | },
303 | }
304 |
305 | for _, tt := range tests {
306 | t.Run(tt.name, func(t *testing.T) {
307 | result := make(map[string]interface{})
308 | request := &mcpgo.CallToolRequest{
309 | Arguments: tt.args,
310 | }
311 | validator := NewValidator(request)
312 |
313 | validator.ValidateAndAddPagination(result)
314 |
315 | if tt.expectError {
316 | assert.True(t, validator.HasErrors(), "Expected validation error")
317 | } else {
318 | assert.False(t, validator.HasErrors(), "Did not expect validation error")
319 | }
320 |
321 | if tt.expectCount != nil {
322 | assert.Equal(t, tt.expectCount, result["count"], "Count mismatch")
323 | } else {
324 | _, exists := result["count"]
325 | assert.False(t, exists, "Count should not be added")
326 | }
327 |
328 | if tt.expectSkip != nil {
329 | assert.Equal(t, tt.expectSkip, result["skip"], "Skip mismatch")
330 | } else {
331 | _, exists := result["skip"]
332 | assert.False(t, exists, "Skip should not be added")
333 | }
334 | })
335 | }
336 | }
337 |
338 | func TestValidatorExpand(t *testing.T) {
339 | tests := []struct {
340 | name string
341 | args map[string]interface{}
342 | expectExpand string
343 | expectError bool
344 | }{
345 | {
346 | name: "valid expand param",
347 | args: map[string]interface{}{"expand": []interface{}{"payments"}},
348 | expectExpand: "payments",
349 | expectError: false,
350 | },
351 | {
352 | name: "empty expand array",
353 | args: map[string]interface{}{"expand": []interface{}{}},
354 | expectExpand: "",
355 | expectError: false,
356 | },
357 | {
358 | name: "invalid expand type",
359 | args: map[string]interface{}{"expand": "not an array"},
360 | expectExpand: "",
361 | expectError: true,
362 | },
363 | }
364 |
365 | for _, tt := range tests {
366 | t.Run(tt.name, func(t *testing.T) {
367 | result := make(map[string]interface{})
368 | request := &mcpgo.CallToolRequest{
369 | Arguments: tt.args,
370 | }
371 | validator := NewValidator(request)
372 |
373 | validator.ValidateAndAddExpand(result)
374 |
375 | if tt.expectError {
376 | assert.True(t, validator.HasErrors(), "Expected validation error")
377 | } else {
378 | assert.False(t, validator.HasErrors(), "Did not expect validation error")
379 | if tt.expectExpand != "" {
380 | assert.Equal(t,
381 | tt.expectExpand,
382 | result["expand[]"],
383 | "Expand value mismatch",
384 | )
385 | } else {
386 | _, exists := result["expand[]"]
387 | assert.False(t, exists, "Expand should not be added")
388 | }
389 | }
390 | })
391 | }
392 | }
393 |
394 | // Test validator "To" functions which write to target maps
395 | func TestValidatorToFunctions(t *testing.T) {
396 | tests := []struct {
397 | name string
398 | args map[string]interface{}
399 | paramName string
400 | targetKey string
401 | testFunc func(
402 | *Validator, map[string]interface{}, string, string,
403 | ) *Validator
404 | expectValue interface{}
405 | expectError bool
406 | }{
407 | // ValidateAndAddOptionalStringToPath tests
408 | {
409 | name: "optional string to target - valid",
410 | args: map[string]interface{}{"customer_name": "Test User"},
411 | paramName: "customer_name",
412 | targetKey: "name",
413 | testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
414 | expectValue: "Test User",
415 | expectError: false,
416 | },
417 | {
418 | name: "optional string to target - empty",
419 | args: map[string]interface{}{"customer_name": ""},
420 | paramName: "customer_name",
421 | targetKey: "name",
422 | testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
423 | expectValue: "",
424 | expectError: false,
425 | },
426 | {
427 | name: "optional string to target - missing",
428 | args: map[string]interface{}{},
429 | paramName: "customer_name",
430 | targetKey: "name",
431 | testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
432 | expectValue: nil,
433 | expectError: false,
434 | },
435 | {
436 | name: "optional string to target - wrong type",
437 | args: map[string]interface{}{"customer_name": 123},
438 | paramName: "customer_name",
439 | targetKey: "name",
440 | testFunc: (*Validator).ValidateAndAddOptionalStringToPath,
441 | expectValue: nil,
442 | expectError: true,
443 | },
444 |
445 | // ValidateAndAddOptionalBoolToPath tests
446 | {
447 | name: "optional bool to target - true",
448 | args: map[string]interface{}{"notify_sms": true},
449 | paramName: "notify_sms",
450 | targetKey: "sms",
451 | testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
452 | expectValue: true,
453 | expectError: false,
454 | },
455 | {
456 | name: "optional bool to target - false",
457 | args: map[string]interface{}{"notify_sms": false},
458 | paramName: "notify_sms",
459 | targetKey: "sms",
460 | testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
461 | expectValue: false,
462 | expectError: false,
463 | },
464 | {
465 | name: "optional bool to target - wrong type",
466 | args: map[string]interface{}{"notify_sms": "not a bool"},
467 | paramName: "notify_sms",
468 | targetKey: "sms",
469 | testFunc: (*Validator).ValidateAndAddOptionalBoolToPath,
470 | expectValue: nil,
471 | expectError: true,
472 | },
473 |
474 | // ValidateAndAddOptionalIntToPath tests
475 | {
476 | name: "optional int to target - valid",
477 | args: map[string]interface{}{"age": float64(25)},
478 | paramName: "age",
479 | targetKey: "customer_age",
480 | testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
481 | expectValue: int64(25),
482 | expectError: false,
483 | },
484 | {
485 | name: "optional int to target - zero",
486 | args: map[string]interface{}{"age": float64(0)},
487 | paramName: "age",
488 | targetKey: "customer_age",
489 | testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
490 | expectValue: int64(0),
491 | expectError: false,
492 | },
493 | {
494 | name: "optional int to target - missing",
495 | args: map[string]interface{}{},
496 | paramName: "age",
497 | targetKey: "customer_age",
498 | testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
499 | expectValue: nil,
500 | expectError: false,
501 | },
502 | {
503 | name: "optional int to target - wrong type",
504 | args: map[string]interface{}{"age": "not a number"},
505 | paramName: "age",
506 | targetKey: "customer_age",
507 | testFunc: (*Validator).ValidateAndAddOptionalIntToPath,
508 | expectValue: nil,
509 | expectError: true,
510 | },
511 | }
512 |
513 | for _, tt := range tests {
514 | t.Run(tt.name, func(t *testing.T) {
515 | // Create a target map for this specific test
516 | target := make(map[string]interface{})
517 |
518 | // Create the request and validator
519 | request := &mcpgo.CallToolRequest{
520 | Arguments: tt.args,
521 | }
522 | validator := NewValidator(request)
523 |
524 | // Call the test function with target and verify its return value
525 | tt.testFunc(validator, target, tt.paramName, tt.targetKey)
526 |
527 | // Check if we got the expected errors
528 | if tt.expectError {
529 | assert.True(t, validator.HasErrors(), "Expected validation error")
530 | } else {
531 | assert.False(t, validator.HasErrors(), "Did not expect validation error")
532 |
533 | // For non-error cases, check target map value
534 | if tt.expectValue != nil {
535 | // Should have the value with the target key
536 | assert.Equal(t,
537 | tt.expectValue,
538 | target[tt.targetKey],
539 | "Target map value mismatch")
540 | } else {
541 | // Target key should not exist
542 | _, exists := target[tt.targetKey]
543 | assert.False(t, exists, "Key should not be in target map when value is empty") // nolint:lll
544 | }
545 | }
546 | })
547 | }
548 | }
549 |
550 | // Test for nested validation with multiple fields into target maps
551 | func TestValidatorNestedObjects(t *testing.T) {
552 | t.Run("customer object validation", func(t *testing.T) {
553 | // Create request with customer details
554 | args := map[string]interface{}{
555 | "customer_name": "John Doe",
556 | "customer_email": "[email protected]",
557 | "customer_contact": "+1234567890",
558 | }
559 | request := &mcpgo.CallToolRequest{
560 | Arguments: args,
561 | }
562 |
563 | // Customer target map
564 | customer := make(map[string]interface{})
565 |
566 | // Create validator and validate customer fields
567 | validator := NewValidator(request).
568 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
569 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email").
570 | ValidateAndAddOptionalStringToPath(customer, "customer_contact", "contact")
571 |
572 | // Should not have errors
573 | assert.False(t, validator.HasErrors())
574 |
575 | // Customer map should have all three fields
576 | assert.Equal(t, "John Doe", customer["name"])
577 | assert.Equal(t, "[email protected]", customer["email"])
578 | assert.Equal(t, "+1234567890", customer["contact"])
579 | })
580 |
581 | t.Run("notification object validation", func(t *testing.T) {
582 | // Create request with notification settings
583 | args := map[string]interface{}{
584 | "notify_sms": true,
585 | "notify_email": false,
586 | }
587 | request := &mcpgo.CallToolRequest{
588 | Arguments: args,
589 | }
590 |
591 | // Notify target map
592 | notify := make(map[string]interface{})
593 |
594 | // Create validator and validate notification fields
595 | validator := NewValidator(request).
596 | ValidateAndAddOptionalBoolToPath(notify, "notify_sms", "sms").
597 | ValidateAndAddOptionalBoolToPath(notify, "notify_email", "email")
598 |
599 | // Should not have errors
600 | assert.False(t, validator.HasErrors())
601 |
602 | // Notify map should have both fields
603 | assert.Equal(t, true, notify["sms"])
604 | assert.Equal(t, false, notify["email"])
605 | })
606 |
607 | t.Run("mixed object with error", func(t *testing.T) {
608 | // Create request with mixed valid and invalid data
609 | args := map[string]interface{}{
610 | "customer_name": "Jane Doe",
611 | "customer_email": 12345, // Wrong type
612 | }
613 | request := &mcpgo.CallToolRequest{
614 | Arguments: args,
615 | }
616 |
617 | // Target map
618 | customer := make(map[string]interface{})
619 |
620 | // Create validator and validate fields
621 | validator := NewValidator(request).
622 | ValidateAndAddOptionalStringToPath(customer, "customer_name", "name").
623 | ValidateAndAddOptionalStringToPath(customer, "customer_email", "email")
624 |
625 | // Should have errors
626 | assert.True(t, validator.HasErrors())
627 |
628 | // Customer map should have only the valid field
629 | assert.Equal(t, "Jane Doe", customer["name"])
630 | _, hasEmail := customer["email"]
631 | assert.False(t, hasEmail, "Invalid field should not be added to target map")
632 | })
633 | }
634 |
635 | // Test for optional bool handling
636 | func TestOptionalBoolBehavior(t *testing.T) {
637 | t.Run("explicit bool values", func(t *testing.T) {
638 | // Create request with explicit bool values
639 | args := map[string]interface{}{
640 | "true_param": true,
641 | "false_param": false,
642 | }
643 | request := &mcpgo.CallToolRequest{
644 | Arguments: args,
645 | }
646 |
647 | // Create result map
648 | result := make(map[string]interface{})
649 |
650 | // Validate both parameters
651 | validator := NewValidator(request).
652 | ValidateAndAddOptionalBool(result, "true_param").
653 | ValidateAndAddOptionalBool(result, "false_param")
654 |
655 | // Verify no errors occurred
656 | assert.False(t, validator.HasErrors())
657 |
658 | // Both parameters should be set in the result
659 | assert.Equal(t, true, result["true_param"])
660 | assert.Equal(t, false, result["false_param"])
661 | })
662 |
663 | t.Run("missing bool parameter", func(t *testing.T) {
664 | // Create request without bool parameters
665 | args := map[string]interface{}{
666 | "other_param": "some value",
667 | }
668 | request := &mcpgo.CallToolRequest{
669 | Arguments: args,
670 | }
671 |
672 | // Create result map
673 | result := make(map[string]interface{})
674 |
675 | // Try to validate missing bool parameters
676 | validator := NewValidator(request).
677 | ValidateAndAddOptionalBool(result, "true_param").
678 | ValidateAndAddOptionalBool(result, "false_param")
679 |
680 | // Verify no errors occurred
681 | assert.False(t, validator.HasErrors())
682 |
683 | // Result should be empty since no bool values were provided
684 | assert.Empty(t, result)
685 | })
686 |
687 | t.Run("explicit bool values with 'To' functions", func(t *testing.T) {
688 | // Create request with explicit bool values
689 | args := map[string]interface{}{
690 | "notify_sms": true,
691 | "notify_email": false,
692 | }
693 | request := &mcpgo.CallToolRequest{
694 | Arguments: args,
695 | }
696 |
697 | // Create target map
698 | target := make(map[string]interface{})
699 |
700 | // Validate both parameters
701 | validator := NewValidator(request).
702 | ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
703 | ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
704 |
705 | // Verify no errors occurred
706 | assert.False(t, validator.HasErrors())
707 |
708 | // Both parameters should be set in the target map
709 | assert.Equal(t, true, target["sms"])
710 | assert.Equal(t, false, target["email"])
711 | })
712 |
713 | t.Run("missing bool parameter with 'To' functions", func(t *testing.T) {
714 | // Create request without bool parameters
715 | args := map[string]interface{}{
716 | "other_param": "some value",
717 | }
718 | request := &mcpgo.CallToolRequest{
719 | Arguments: args,
720 | }
721 |
722 | // Create target map
723 | target := make(map[string]interface{})
724 |
725 | // Try to validate missing bool parameters
726 | validator := NewValidator(request).
727 | ValidateAndAddOptionalBoolToPath(target, "notify_sms", "sms").
728 | ValidateAndAddOptionalBoolToPath(target, "notify_email", "email")
729 |
730 | // Verify no errors occurred
731 | assert.False(t, validator.HasErrors())
732 |
733 | // Target map should be empty since no bool values were provided
734 | assert.Empty(t, target)
735 | })
736 | }
737 |
738 | // Test for extractValueGeneric function edge cases
739 | func TestExtractValueGeneric(t *testing.T) {
740 | t.Run("invalid arguments type", func(t *testing.T) {
741 | request := &mcpgo.CallToolRequest{
742 | Arguments: "invalid_type", // Not a map
743 | }
744 |
745 | result, err := extractValueGeneric[string](request, "test", false)
746 | assert.Error(t, err)
747 | assert.Equal(t, "invalid arguments type", err.Error())
748 | assert.Nil(t, result)
749 | })
750 |
751 | t.Run("json marshal error", func(t *testing.T) {
752 | // Create a value that can't be marshaled to JSON
753 | args := map[string]interface{}{
754 | "test_param": make(chan int), // Channels can't be marshaled
755 | }
756 | request := &mcpgo.CallToolRequest{
757 | Arguments: args,
758 | }
759 |
760 | result, err := extractValueGeneric[string](request, "test_param", false)
761 | assert.Error(t, err)
762 | assert.Equal(t, "invalid parameter type: test_param", err.Error())
763 | assert.Nil(t, result)
764 | })
765 |
766 | t.Run("json unmarshal error", func(t *testing.T) {
767 | // Provide a value that can't be unmarshaled to the target type
768 | args := map[string]interface{}{
769 | "test_param": []interface{}{1, 2, 3}, // Array can't be unmarshaled to string
770 | }
771 | request := &mcpgo.CallToolRequest{
772 | Arguments: args,
773 | }
774 |
775 | result, err := extractValueGeneric[string](request, "test_param", false)
776 | assert.Error(t, err)
777 | assert.Equal(t, "invalid parameter type: test_param", err.Error())
778 | assert.Nil(t, result)
779 | })
780 | }
781 |
782 | // Test for validateAndAddRequired function
783 | func TestValidateAndAddRequired(t *testing.T) {
784 | t.Run("successful validation", func(t *testing.T) {
785 | args := map[string]interface{}{
786 | "test_param": "test_value",
787 | }
788 | request := &mcpgo.CallToolRequest{
789 | Arguments: args,
790 | }
791 |
792 | params := make(map[string]interface{})
793 | validator := NewValidator(request)
794 |
795 | result := validateAndAddRequired[string](validator, params, "test_param")
796 |
797 | assert.False(t, result.HasErrors())
798 | assert.Equal(t, "test_value", params["test_param"])
799 | })
800 |
801 | t.Run("validation error", func(t *testing.T) {
802 | request := &mcpgo.CallToolRequest{
803 | Arguments: "invalid_type",
804 | }
805 |
806 | params := make(map[string]interface{})
807 | validator := NewValidator(request)
808 |
809 | result := validateAndAddRequired[string](validator, params, "test_param")
810 |
811 | assert.True(t, result.HasErrors())
812 | assert.Empty(t, params)
813 | })
814 |
815 | t.Run("nil value after successful extraction", func(t *testing.T) {
816 | // This edge case is hard to trigger directly, but we can simulate it
817 | // by using a type that extractValueGeneric might return as nil
818 | args := map[string]interface{}{
819 | "test_param": nil,
820 | }
821 | request := &mcpgo.CallToolRequest{
822 | Arguments: args,
823 | }
824 |
825 | params := make(map[string]interface{})
826 | validator := NewValidator(request)
827 |
828 | result := validateAndAddRequired[string](validator, params, "test_param")
829 |
830 | // This should result in an error because the parameter is required
831 | assert.True(t, result.HasErrors())
832 | assert.Empty(t, params)
833 | })
834 | }
835 |
836 | // Test for validateAndAddOptional function
837 | func TestValidateAndAddOptional(t *testing.T) {
838 | t.Run("successful validation", func(t *testing.T) {
839 | args := map[string]interface{}{
840 | "test_param": "test_value",
841 | }
842 | request := &mcpgo.CallToolRequest{
843 | Arguments: args,
844 | }
845 |
846 | params := make(map[string]interface{})
847 | validator := NewValidator(request)
848 |
849 | result := validateAndAddOptional[string](validator, params, "test_param")
850 |
851 | assert.False(t, result.HasErrors())
852 | assert.Equal(t, "test_value", params["test_param"])
853 | })
854 |
855 | t.Run("validation error", func(t *testing.T) {
856 | request := &mcpgo.CallToolRequest{
857 | Arguments: "invalid_type",
858 | }
859 |
860 | params := make(map[string]interface{})
861 | validator := NewValidator(request)
862 |
863 | result := validateAndAddOptional[string](validator, params, "test_param")
864 |
865 | assert.True(t, result.HasErrors())
866 | assert.Empty(t, params)
867 | })
868 |
869 | t.Run("nil value handling", func(t *testing.T) {
870 | args := map[string]interface{}{
871 | "test_param": nil,
872 | }
873 | request := &mcpgo.CallToolRequest{
874 | Arguments: args,
875 | }
876 |
877 | params := make(map[string]interface{})
878 | validator := NewValidator(request)
879 |
880 | result := validateAndAddOptional[string](validator, params, "test_param")
881 |
882 | assert.False(t, result.HasErrors())
883 | assert.Empty(t, params)
884 | })
885 | }
886 |
887 | // Test for validateAndAddToPath function
888 | func TestValidateAndAddToPath(t *testing.T) {
889 | t.Run("successful validation", func(t *testing.T) {
890 | args := map[string]interface{}{
891 | "test_param": "test_value",
892 | }
893 | request := &mcpgo.CallToolRequest{
894 | Arguments: args,
895 | }
896 |
897 | target := make(map[string]interface{})
898 | validator := NewValidator(request)
899 |
900 | result := validateAndAddToPath[string](
901 | validator, target, "test_param", "target_key")
902 |
903 | assert.False(t, result.HasErrors())
904 | assert.Equal(t, "test_value", target["target_key"])
905 | })
906 |
907 | t.Run("validation error", func(t *testing.T) {
908 | request := &mcpgo.CallToolRequest{
909 | Arguments: "invalid_type",
910 | }
911 |
912 | target := make(map[string]interface{})
913 | validator := NewValidator(request)
914 |
915 | result := validateAndAddToPath[string](
916 | validator, target, "test_param", "target_key")
917 |
918 | assert.True(t, result.HasErrors())
919 | assert.Empty(t, target)
920 | })
921 |
922 | t.Run("nil value handling", func(t *testing.T) {
923 | args := map[string]interface{}{
924 | "test_param": nil,
925 | }
926 | request := &mcpgo.CallToolRequest{
927 | Arguments: args,
928 | }
929 |
930 | target := make(map[string]interface{})
931 | validator := NewValidator(request)
932 |
933 | result := validateAndAddToPath[string](
934 | validator, target, "test_param", "target_key")
935 |
936 | assert.False(t, result.HasErrors())
937 | assert.Empty(t, target)
938 | })
939 | }
940 |
941 | // Test for ValidateAndAddPagination function
942 | func TestValidateAndAddPagination(t *testing.T) {
943 | t.Run("all pagination parameters", func(t *testing.T) {
944 | args := map[string]interface{}{
945 | "count": 10,
946 | "skip": 5,
947 | }
948 | request := &mcpgo.CallToolRequest{
949 | Arguments: args,
950 | }
951 |
952 | params := make(map[string]interface{})
953 | validator := NewValidator(request).ValidateAndAddPagination(params)
954 |
955 | assert.False(t, validator.HasErrors())
956 | assert.Equal(t, int64(10), params["count"])
957 | assert.Equal(t, int64(5), params["skip"])
958 | })
959 |
960 | t.Run("missing pagination parameters", func(t *testing.T) {
961 | args := map[string]interface{}{}
962 | request := &mcpgo.CallToolRequest{
963 | Arguments: args,
964 | }
965 |
966 | params := make(map[string]interface{})
967 | validator := NewValidator(request).ValidateAndAddPagination(params)
968 |
969 | assert.False(t, validator.HasErrors())
970 | assert.Empty(t, params)
971 | })
972 |
973 | t.Run("invalid count type", func(t *testing.T) {
974 | args := map[string]interface{}{
975 | "count": "invalid",
976 | }
977 | request := &mcpgo.CallToolRequest{
978 | Arguments: args,
979 | }
980 |
981 | params := make(map[string]interface{})
982 | validator := NewValidator(request).ValidateAndAddPagination(params)
983 |
984 | assert.True(t, validator.HasErrors())
985 | })
986 | }
987 |
988 | // Test for ValidateAndAddExpand function
989 | func TestValidateAndAddExpand(t *testing.T) {
990 | t.Run("valid expand parameter", func(t *testing.T) {
991 | args := map[string]interface{}{
992 | "expand": []string{"payments", "customer"},
993 | }
994 | request := &mcpgo.CallToolRequest{
995 | Arguments: args,
996 | }
997 |
998 | params := make(map[string]interface{})
999 | validator := NewValidator(request).ValidateAndAddExpand(params)
1000 |
1001 | assert.False(t, validator.HasErrors())
1002 | // The function sets expand[] for each value, so check the last one
1003 | assert.Equal(t, "customer", params["expand[]"])
1004 | })
1005 |
1006 | t.Run("missing expand parameter", func(t *testing.T) {
1007 | args := map[string]interface{}{}
1008 | request := &mcpgo.CallToolRequest{
1009 | Arguments: args,
1010 | }
1011 |
1012 | params := make(map[string]interface{})
1013 | validator := NewValidator(request).ValidateAndAddExpand(params)
1014 |
1015 | assert.False(t, validator.HasErrors())
1016 | assert.Empty(t, params)
1017 | })
1018 |
1019 | t.Run("invalid expand type", func(t *testing.T) {
1020 | args := map[string]interface{}{
1021 | "expand": "invalid", // Should be []string, not string
1022 | }
1023 | request := &mcpgo.CallToolRequest{
1024 | Arguments: args,
1025 | }
1026 |
1027 | params := make(map[string]interface{})
1028 | validator := NewValidator(request).ValidateAndAddExpand(params)
1029 |
1030 | assert.True(t, validator.HasErrors())
1031 | })
1032 | }
1033 |
1034 | // Test for token validation functions edge cases
1035 | func TestTokenValidationEdgeCases(t *testing.T) {
1036 | t.Run("validateTokenMaxAmount - int conversion", func(t *testing.T) {
1037 | token := map[string]interface{}{
1038 | "max_amount": 100, // int instead of float64
1039 | }
1040 |
1041 | request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1042 | validator := NewValidator(request).validateTokenMaxAmount(token)
1043 |
1044 | assert.False(t, validator.HasErrors())
1045 | assert.Equal(t, float64(100), token["max_amount"])
1046 | })
1047 |
1048 | t.Run("validateTokenExpireAt - int conversion", func(t *testing.T) {
1049 | token := map[string]interface{}{
1050 | "expire_at": 1234567890, // int instead of float64
1051 | }
1052 |
1053 | request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1054 | validator := NewValidator(request).validateTokenExpireAt(token)
1055 |
1056 | assert.False(t, validator.HasErrors())
1057 | assert.Equal(t, float64(1234567890), token["expire_at"])
1058 | })
1059 |
1060 | t.Run("validateTokenExpireAt - zero value", func(t *testing.T) {
1061 | token := map[string]interface{}{
1062 | "expire_at": 0,
1063 | }
1064 |
1065 | request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1066 | validator := NewValidator(request).validateTokenExpireAt(token)
1067 |
1068 | assert.True(t, validator.HasErrors())
1069 | })
1070 |
1071 | t.Run("validateTokenMaxAmount - zero value", func(t *testing.T) {
1072 | token := map[string]interface{}{
1073 | "max_amount": 0,
1074 | }
1075 |
1076 | request := &mcpgo.CallToolRequest{Arguments: map[string]interface{}{}}
1077 | validator := NewValidator(request).validateTokenMaxAmount(token)
1078 |
1079 | assert.True(t, validator.HasErrors())
1080 | })
1081 | }
1082 |
1083 | // Test for ValidateAndAddToken edge cases
1084 | func TestValidateAndAddTokenEdgeCases(t *testing.T) {
1085 | t.Run("token extraction error", func(t *testing.T) {
1086 | request := &mcpgo.CallToolRequest{
1087 | Arguments: "invalid_type",
1088 | }
1089 |
1090 | params := make(map[string]interface{})
1091 | validator := NewValidator(request).ValidateAndAddToken(params, "token")
1092 |
1093 | assert.True(t, validator.HasErrors())
1094 | assert.Empty(t, params)
1095 | })
1096 |
1097 | t.Run("nil token value", func(t *testing.T) {
1098 | args := map[string]interface{}{
1099 | "token": nil,
1100 | }
1101 | request := &mcpgo.CallToolRequest{
1102 | Arguments: args,
1103 | }
1104 |
1105 | params := make(map[string]interface{})
1106 | validator := NewValidator(request).ValidateAndAddToken(params, "token")
1107 |
1108 | assert.False(t, validator.HasErrors())
1109 | assert.Empty(t, params)
1110 | })
1111 |
1112 | t.Run("token validation errors", func(t *testing.T) {
1113 | args := map[string]interface{}{
1114 | "token": map[string]interface{}{
1115 | "max_amount": -100, // Invalid value
1116 | },
1117 | }
1118 | request := &mcpgo.CallToolRequest{
1119 | Arguments: args,
1120 | }
1121 |
1122 | params := make(map[string]interface{})
1123 | validator := NewValidator(request).ValidateAndAddToken(params, "token")
1124 |
1125 | assert.True(t, validator.HasErrors())
1126 | assert.Empty(t, params)
1127 | })
1128 | }
1129 |
```