|
5 | 5 | "encoding/json"
|
6 | 6 | "net/http"
|
7 | 7 | "testing"
|
| 8 | +"time" |
8 | 9 |
|
9 | 10 | "github.com/google/go-github/v69/github"
|
10 | 11 | "github.com/mark3labs/mcp-go/mcp"
|
@@ -524,3 +525,219 @@ func Test_CreateIssue(t *testing.T) {
|
524 | 525 | })
|
525 | 526 | }
|
526 | 527 | }
|
| 528 | + |
| 529 | +funcTest_ListIssues(t*testing.T) { |
| 530 | +// Verify tool definition |
| 531 | +mockClient:=github.NewClient(nil) |
| 532 | +tool,_:=listIssues(mockClient) |
| 533 | + |
| 534 | +assert.Equal(t,"list_issues",tool.Name) |
| 535 | +assert.NotEmpty(t,tool.Description) |
| 536 | +assert.Contains(t,tool.InputSchema.Properties,"owner") |
| 537 | +assert.Contains(t,tool.InputSchema.Properties,"repo") |
| 538 | +assert.Contains(t,tool.InputSchema.Properties,"state") |
| 539 | +assert.Contains(t,tool.InputSchema.Properties,"labels") |
| 540 | +assert.Contains(t,tool.InputSchema.Properties,"sort") |
| 541 | +assert.Contains(t,tool.InputSchema.Properties,"direction") |
| 542 | +assert.Contains(t,tool.InputSchema.Properties,"since") |
| 543 | +assert.Contains(t,tool.InputSchema.Properties,"page") |
| 544 | +assert.Contains(t,tool.InputSchema.Properties,"per_page") |
| 545 | +assert.ElementsMatch(t,tool.InputSchema.Required, []string{"owner","repo"}) |
| 546 | + |
| 547 | +// Setup mock issues for success case |
| 548 | +mockIssues:= []*github.Issue{ |
| 549 | +{ |
| 550 | +Number:github.Ptr(123), |
| 551 | +Title:github.Ptr("First Issue"), |
| 552 | +Body:github.Ptr("This is the first test issue"), |
| 553 | +State:github.Ptr("open"), |
| 554 | +HTMLURL:github.Ptr("https://github.com/owner/repo/issues/123"), |
| 555 | +CreatedAt:&github.Timestamp{Time:time.Date(2023,1,1,0,0,0,0,time.UTC)}, |
| 556 | +}, |
| 557 | +{ |
| 558 | +Number:github.Ptr(456), |
| 559 | +Title:github.Ptr("Second Issue"), |
| 560 | +Body:github.Ptr("This is the second test issue"), |
| 561 | +State:github.Ptr("open"), |
| 562 | +HTMLURL:github.Ptr("https://github.com/owner/repo/issues/456"), |
| 563 | +Labels: []*github.Label{{Name:github.Ptr("bug")}}, |
| 564 | +CreatedAt:&github.Timestamp{Time:time.Date(2023,2,1,0,0,0,0,time.UTC)}, |
| 565 | +}, |
| 566 | +} |
| 567 | + |
| 568 | +tests:= []struct { |
| 569 | +namestring |
| 570 | +mockedClient*http.Client |
| 571 | +requestArgsmap[string]interface{} |
| 572 | +expectErrorbool |
| 573 | +expectedIssues []*github.Issue |
| 574 | +expectedErrMsgstring |
| 575 | +}{ |
| 576 | +{ |
| 577 | +name:"list issues with minimal parameters", |
| 578 | +mockedClient:mock.NewMockedHTTPClient( |
| 579 | +mock.WithRequestMatch( |
| 580 | +mock.GetReposIssuesByOwnerByRepo, |
| 581 | +mockIssues, |
| 582 | +), |
| 583 | +), |
| 584 | +requestArgs:map[string]interface{}{ |
| 585 | +"owner":"owner", |
| 586 | +"repo":"repo", |
| 587 | +}, |
| 588 | +expectError:false, |
| 589 | +expectedIssues:mockIssues, |
| 590 | +}, |
| 591 | +{ |
| 592 | +name:"list issues with all parameters", |
| 593 | +mockedClient:mock.NewMockedHTTPClient( |
| 594 | +mock.WithRequestMatch( |
| 595 | +mock.GetReposIssuesByOwnerByRepo, |
| 596 | +mockIssues, |
| 597 | +), |
| 598 | +), |
| 599 | +requestArgs:map[string]interface{}{ |
| 600 | +"owner":"owner", |
| 601 | +"repo":"repo", |
| 602 | +"state":"open", |
| 603 | +"labels":"bug,enhancement", |
| 604 | +"sort":"created", |
| 605 | +"direction":"desc", |
| 606 | +"since":"2023-01-01T00:00:00Z", |
| 607 | +"page":float64(1), |
| 608 | +"per_page":float64(30), |
| 609 | +}, |
| 610 | +expectError:false, |
| 611 | +expectedIssues:mockIssues, |
| 612 | +}, |
| 613 | +{ |
| 614 | +name:"invalid since parameter", |
| 615 | +mockedClient:mock.NewMockedHTTPClient( |
| 616 | +mock.WithRequestMatch( |
| 617 | +mock.GetReposIssuesByOwnerByRepo, |
| 618 | +mockIssues, |
| 619 | +), |
| 620 | +), |
| 621 | +requestArgs:map[string]interface{}{ |
| 622 | +"owner":"owner", |
| 623 | +"repo":"repo", |
| 624 | +"since":"invalid-date", |
| 625 | +}, |
| 626 | +expectError:true, |
| 627 | +expectedErrMsg:"invalid ISO 8601 timestamp", |
| 628 | +}, |
| 629 | +{ |
| 630 | +name:"list issues fails with error", |
| 631 | +mockedClient:mock.NewMockedHTTPClient( |
| 632 | +mock.WithRequestMatchHandler( |
| 633 | +mock.GetReposIssuesByOwnerByRepo, |
| 634 | +http.HandlerFunc(func(w http.ResponseWriter,r*http.Request) { |
| 635 | +w.WriteHeader(http.StatusNotFound) |
| 636 | +_,_=w.Write([]byte(`{"message": "Repository not found"}`)) |
| 637 | +}), |
| 638 | +), |
| 639 | +), |
| 640 | +requestArgs:map[string]interface{}{ |
| 641 | +"owner":"nonexistent", |
| 642 | +"repo":"repo", |
| 643 | +}, |
| 644 | +expectError:true, |
| 645 | +expectedErrMsg:"failed to list issues", |
| 646 | +}, |
| 647 | +} |
| 648 | + |
| 649 | +for_,tc:=rangetests { |
| 650 | +t.Run(tc.name,func(t*testing.T) { |
| 651 | +// Setup client with mock |
| 652 | +client:=github.NewClient(tc.mockedClient) |
| 653 | +_,handler:=listIssues(client) |
| 654 | + |
| 655 | +// Create call request |
| 656 | +request:=createMCPRequest(tc.requestArgs) |
| 657 | + |
| 658 | +// Call handler |
| 659 | +result,err:=handler(context.Background(),request) |
| 660 | + |
| 661 | +// Verify results |
| 662 | +iftc.expectError { |
| 663 | +iferr!=nil { |
| 664 | +assert.Contains(t,err.Error(),tc.expectedErrMsg) |
| 665 | +}else { |
| 666 | +// For errors returned as part of the result, not as an error |
| 667 | +assert.NotNil(t,result) |
| 668 | +textContent:=getTextResult(t,result) |
| 669 | +assert.Contains(t,textContent.Text,tc.expectedErrMsg) |
| 670 | +} |
| 671 | +return |
| 672 | +} |
| 673 | + |
| 674 | +require.NoError(t,err) |
| 675 | + |
| 676 | +// Parse the result and get the text content if no error |
| 677 | +textContent:=getTextResult(t,result) |
| 678 | + |
| 679 | +// Unmarshal and verify the result |
| 680 | +varreturnedIssues []*github.Issue |
| 681 | +err=json.Unmarshal([]byte(textContent.Text),&returnedIssues) |
| 682 | +require.NoError(t,err) |
| 683 | + |
| 684 | +assert.Len(t,returnedIssues,len(tc.expectedIssues)) |
| 685 | +fori,issue:=rangereturnedIssues { |
| 686 | +assert.Equal(t,*tc.expectedIssues[i].Number,*issue.Number) |
| 687 | +assert.Equal(t,*tc.expectedIssues[i].Title,*issue.Title) |
| 688 | +assert.Equal(t,*tc.expectedIssues[i].State,*issue.State) |
| 689 | +assert.Equal(t,*tc.expectedIssues[i].HTMLURL,*issue.HTMLURL) |
| 690 | +} |
| 691 | +}) |
| 692 | +} |
| 693 | +} |
| 694 | + |
| 695 | +funcTest_ParseISOTimestamp(t*testing.T) { |
| 696 | +tests:= []struct { |
| 697 | +namestring |
| 698 | +inputstring |
| 699 | +expectedErrbool |
| 700 | +expectedTime time.Time |
| 701 | +}{ |
| 702 | +{ |
| 703 | +name:"valid RFC3339 format", |
| 704 | +input:"2023-01-15T14:30:00Z", |
| 705 | +expectedErr:false, |
| 706 | +expectedTime:time.Date(2023,1,15,14,30,0,0,time.UTC), |
| 707 | +}, |
| 708 | +{ |
| 709 | +name:"valid date only format", |
| 710 | +input:"2023-01-15", |
| 711 | +expectedErr:false, |
| 712 | +expectedTime:time.Date(2023,1,15,0,0,0,0,time.UTC), |
| 713 | +}, |
| 714 | +{ |
| 715 | +name:"empty timestamp", |
| 716 | +input:"", |
| 717 | +expectedErr:true, |
| 718 | +}, |
| 719 | +{ |
| 720 | +name:"invalid format", |
| 721 | +input:"15/01/2023", |
| 722 | +expectedErr:true, |
| 723 | +}, |
| 724 | +{ |
| 725 | +name:"invalid date", |
| 726 | +input:"2023-13-45", |
| 727 | +expectedErr:true, |
| 728 | +}, |
| 729 | +} |
| 730 | + |
| 731 | +for_,tc:=rangetests { |
| 732 | +t.Run(tc.name,func(t*testing.T) { |
| 733 | +parsedTime,err:=parseISOTimestamp(tc.input) |
| 734 | + |
| 735 | +iftc.expectedErr { |
| 736 | +assert.Error(t,err) |
| 737 | +}else { |
| 738 | +assert.NoError(t,err) |
| 739 | +assert.Equal(t,tc.expectedTime,parsedTime) |
| 740 | +} |
| 741 | +}) |
| 742 | +} |
| 743 | +} |