|
1 | 1 | import {
|
| 2 | + afterAll, |
| 3 | + afterEach, |
| 4 | + beforeEach, |
2 | 5 | describe,
|
| 6 | + expect, |
3 | 7 | it,
|
4 | 8 | test,
|
5 |
| - expect, |
6 |
| - beforeEach, |
7 | 9 | vi,
|
8 |
| - afterAll, |
9 |
| - afterEach, |
10 | 10 | } from "vitest";
|
11 | 11 |
|
12 | 12 | global.fetch = vi.fn();
|
13 | 13 |
|
14 |
| -import { shopifyTools, searchShopifyDocs } from "./index.js"; |
15 | 14 | import {
|
16 | 15 | instrumentationData,
|
17 | 16 | isInstrumentationDisabled,
|
18 | 17 | } from "../instrumentation.js";
|
19 |
| -import { searchShopifyAdminSchema } from "./shopify-admin-schema.js"; |
| 18 | +import { ValidationResult } from "../types.js"; |
| 19 | +import validateGraphQLOperation from "../validations/graphqlSchema.js"; |
| 20 | +import { searchShopifyDocs, shopifyTools } from "./index.js"; |
| 21 | +import { searchShopifyAdminSchema } from "./shopifyAdminSchema.js"; |
20 | 22 |
|
21 | 23 | const originalConsoleError = console.error;
|
22 | 24 | const originalConsoleWarn = console.warn;
|
@@ -78,10 +80,15 @@ vi.mock("../instrumentation.js", () => ({
|
78 | 80 | }));
|
79 | 81 |
|
80 | 82 | // Mock searchShopifyAdminSchema
|
81 |
| -vi.mock("./shopify-admin-schema.js", () => ({ |
| 83 | +vi.mock("./shopifyAdminSchema.js", () => ({ |
82 | 84 | searchShopifyAdminSchema: vi.fn(),
|
83 | 85 | }));
|
84 | 86 |
|
| 87 | +// Mock validateGraphQLOperation |
| 88 | +vi.mock("../validations/graphqlSchema.js", () => ({ |
| 89 | + default: vi.fn(), |
| 90 | +})); |
| 91 | + |
85 | 92 | vi.mock("../../package.json", () => ({
|
86 | 93 | default: { version: "1.0.0" },
|
87 | 94 | }));
|
@@ -609,3 +616,249 @@ describe("get_started tool behavior", () => {
|
609 | 616 | expect(result.content[0].text).toContain("Network failure");
|
610 | 617 | });
|
611 | 618 | });
|
| 619 | + |
| 620 | +describe("validate_graphql tool", () => { |
| 621 | + let mockServer: any; |
| 622 | + let validateGraphQLOperationMock: any; |
| 623 | + |
| 624 | + beforeEach(() => { |
| 625 | + vi.clearAllMocks(); |
| 626 | + validateGraphQLOperationMock = vi.mocked(validateGraphQLOperation); |
| 627 | + |
| 628 | + // Mock fetch for getting started APIs |
| 629 | + const fetchMock = global.fetch as any; |
| 630 | + fetchMock.mockResolvedValue({ |
| 631 | + ok: true, |
| 632 | + status: 200, |
| 633 | + text: async () => JSON.stringify(sampleGettingStartedApisResponse), |
| 634 | + }); |
| 635 | + |
| 636 | + // Create a mock server that captures the registered tools |
| 637 | + mockServer = { |
| 638 | + tool: vi.fn((name, description, schema, handler) => { |
| 639 | + if (name === "validate_graphql") { |
| 640 | + mockServer.validateHandler = handler; |
| 641 | + } |
| 642 | + }), |
| 643 | + validateHandler: null, |
| 644 | + }; |
| 645 | + |
| 646 | + // Mock instrumentation |
| 647 | + vi.mocked(instrumentationData).mockReturnValue({ |
| 648 | + packageVersion: "1.0.0", |
| 649 | + timestamp: "2024-01-01T00:00:00.000Z", |
| 650 | + }); |
| 651 | + vi.mocked(isInstrumentationDisabled).mockReturnValue(false); |
| 652 | + }); |
| 653 | + |
| 654 | + test("validates multiple code snippets successfully", async () => { |
| 655 | + // Setup mock responses |
| 656 | + validateGraphQLOperationMock |
| 657 | + .mockResolvedValueOnce({ |
| 658 | + result: ValidationResult.SUCCESS, |
| 659 | + resultDetail: |
| 660 | + "Successfully validated GraphQL query against Shopify Admin API schema.", |
| 661 | + }) |
| 662 | + .mockResolvedValueOnce({ |
| 663 | + result: ValidationResult.FAILED, |
| 664 | + resultDetail: |
| 665 | + "No GraphQL operation found in the provided code snippet.", |
| 666 | + }); |
| 667 | + |
| 668 | + // Register the tools |
| 669 | + await shopifyTools(mockServer); |
| 670 | + |
| 671 | + // Ensure the handler was registered |
| 672 | + expect(mockServer.validateHandler).not.toBeNull(); |
| 673 | + |
| 674 | + const testCodeSnippets = ["query { products { id } }", "const x = 1;"]; |
| 675 | + |
| 676 | + // Call the handler |
| 677 | + const result = await mockServer.validateHandler({ |
| 678 | + code: testCodeSnippets, |
| 679 | + api: "admin", |
| 680 | + }); |
| 681 | + |
| 682 | + // Verify validateGraphQLOperation was called correctly |
| 683 | + expect(validateGraphQLOperationMock).toHaveBeenCalledTimes(2); |
| 684 | + expect(validateGraphQLOperationMock).toHaveBeenNthCalledWith( |
| 685 | + 1, |
| 686 | + testCodeSnippets[0], |
| 687 | + "admin", |
| 688 | + ); |
| 689 | + expect(validateGraphQLOperationMock).toHaveBeenNthCalledWith( |
| 690 | + 2, |
| 691 | + testCodeSnippets[1], |
| 692 | + "admin", |
| 693 | + ); |
| 694 | + |
| 695 | + // Verify the response |
| 696 | + expect(result.content[0].type).toBe("text"); |
| 697 | + const responseText = result.content[0].text; |
| 698 | + expect(responseText).toContain("❌ INVALID"); |
| 699 | + expect(responseText).toContain("**Total Code Snippets:** 2"); |
| 700 | + expect(responseText).toContain("Successfully validated GraphQL query"); |
| 701 | + expect(responseText).toContain("No GraphQL operation found"); |
| 702 | + }); |
| 703 | + |
| 704 | + test("handles validation failures correctly", async () => { |
| 705 | + // Setup mock responses with failures |
| 706 | + validateGraphQLOperationMock |
| 707 | + .mockResolvedValueOnce({ |
| 708 | + result: ValidationResult.FAILED, |
| 709 | + resultDetail: |
| 710 | + "GraphQL validation errors: Cannot query field 'invalidField' on type 'Product'.", |
| 711 | + }) |
| 712 | + .mockResolvedValueOnce({ |
| 713 | + result: ValidationResult.SUCCESS, |
| 714 | + resultDetail: |
| 715 | + "Successfully validated GraphQL mutation against Shopify Admin API schema.", |
| 716 | + }); |
| 717 | + |
| 718 | + // Register the tools |
| 719 | + await shopifyTools(mockServer); |
| 720 | + |
| 721 | + const testCodeSnippets = [ |
| 722 | + "query { products { invalidField } }", |
| 723 | + "mutation { productCreate(input: {}) { product { id } } }", |
| 724 | + ]; |
| 725 | + |
| 726 | + // Call the handler |
| 727 | + const result = await mockServer.validateHandler({ |
| 728 | + code: testCodeSnippets, |
| 729 | + api: "admin", |
| 730 | + }); |
| 731 | + |
| 732 | + // Verify the response shows invalid overall status |
| 733 | + const responseText = result.content[0].text; |
| 734 | + expect(responseText).toContain("❌ INVALID"); |
| 735 | + expect(responseText).toContain("**Total Code Snippets:** 2"); |
| 736 | + expect(responseText).toContain("Cannot query field 'invalidField'"); |
| 737 | + expect(responseText).toContain("Successfully validated GraphQL mutation"); |
| 738 | + }); |
| 739 | + |
| 740 | + test("handles mixed validation results", async () => { |
| 741 | + // Setup mixed results |
| 742 | + validateGraphQLOperationMock |
| 743 | + .mockResolvedValueOnce({ |
| 744 | + result: ValidationResult.SUCCESS, |
| 745 | + resultDetail: |
| 746 | + "Successfully validated GraphQL query against Shopify Admin API schema.", |
| 747 | + }) |
| 748 | + .mockResolvedValueOnce({ |
| 749 | + result: ValidationResult.FAILED, |
| 750 | + resultDetail: |
| 751 | + "No GraphQL operation found in the provided code snippet.", |
| 752 | + }) |
| 753 | + .mockResolvedValueOnce({ |
| 754 | + result: ValidationResult.FAILED, |
| 755 | + resultDetail: |
| 756 | + "GraphQL syntax error: Syntax Error: Expected Name, found }", |
| 757 | + }); |
| 758 | + |
| 759 | + // Register the tools |
| 760 | + await shopifyTools(mockServer); |
| 761 | + |
| 762 | + const testCodeSnippets = [ |
| 763 | + "query { products { id } }", |
| 764 | + "const x = 1;", |
| 765 | + "query { products { } }", |
| 766 | + ]; |
| 767 | + |
| 768 | + // Call the handler |
| 769 | + const result = await mockServer.validateHandler({ |
| 770 | + code: testCodeSnippets, |
| 771 | + api: "admin", |
| 772 | + }); |
| 773 | + |
| 774 | + // Verify the response shows invalid overall status due to failure |
| 775 | + const responseText = result.content[0].text; |
| 776 | + expect(responseText).toContain("❌ INVALID"); |
| 777 | + expect(responseText).toContain("**Total Code Snippets:** 3"); |
| 778 | + expect(responseText).toContain("Code Snippet 1\n**Status:** ✅ SUCCESS"); |
| 779 | + expect(responseText).toContain("Code Snippet 2\n**Status:** ❌ FAILED"); |
| 780 | + expect(responseText).toContain("Code Snippet 3\n**Status:** ❌ FAILED"); |
| 781 | + expect(responseText).toContain("Syntax Error: Expected Name, found }"); |
| 782 | + }); |
| 783 | + |
| 784 | + test("handles empty code snippets array", async () => { |
| 785 | + // Register the tools |
| 786 | + await shopifyTools(mockServer); |
| 787 | + |
| 788 | + // Call the handler with empty array |
| 789 | + const result = await mockServer.validateHandler({ |
| 790 | + code: [], |
| 791 | + api: "admin", |
| 792 | + }); |
| 793 | + |
| 794 | + // Verify validateGraphQLOperation was not called |
| 795 | + expect(validateGraphQLOperationMock).not.toHaveBeenCalled(); |
| 796 | + |
| 797 | + // Verify the response |
| 798 | + const responseText = result.content[0].text; |
| 799 | + expect(responseText).toContain("✅ VALID"); |
| 800 | + expect(responseText).toContain("**Total Code Snippets:** 0"); |
| 801 | + }); |
| 802 | + |
| 803 | + test("handles validation function errors", async () => { |
| 804 | + // Setup mock to throw an error |
| 805 | + validateGraphQLOperationMock.mockRejectedValueOnce( |
| 806 | + new Error("Schema loading failed"), |
| 807 | + ); |
| 808 | + |
| 809 | + // Register the tools |
| 810 | + await shopifyTools(mockServer); |
| 811 | + |
| 812 | + const testCodeSnippets = ["query { products { id } }"]; |
| 813 | + |
| 814 | + // Call the handler and expect it to handle the error gracefully |
| 815 | + await expect( |
| 816 | + mockServer.validateHandler({ |
| 817 | + code: testCodeSnippets, |
| 818 | + api: "admin", |
| 819 | + }), |
| 820 | + ).rejects.toThrow("Schema loading failed"); |
| 821 | + |
| 822 | + // Verify validateGraphQLOperation was called |
| 823 | + expect(validateGraphQLOperationMock).toHaveBeenCalledTimes(1); |
| 824 | + }); |
| 825 | + |
| 826 | + test("records usage data correctly", async () => { |
| 827 | + // Setup successful validation |
| 828 | + validateGraphQLOperationMock.mockResolvedValueOnce({ |
| 829 | + result: ValidationResult.SUCCESS, |
| 830 | + resultDetail: |
| 831 | + "Successfully validated GraphQL query against Shopify Admin API schema.", |
| 832 | + }); |
| 833 | + |
| 834 | + // Mock fetch for usage recording |
| 835 | + const fetchMock = global.fetch as any; |
| 836 | + fetchMock.mockResolvedValue({ |
| 837 | + ok: true, |
| 838 | + status: 200, |
| 839 | + }); |
| 840 | + |
| 841 | + // Register the tools |
| 842 | + await shopifyTools(mockServer); |
| 843 | + |
| 844 | + const testCodeSnippets = ["query { products { id } }"]; |
| 845 | + |
| 846 | + // Call the handler |
| 847 | + await mockServer.validateHandler({ |
| 848 | + code: testCodeSnippets, |
| 849 | + api: "admin", |
| 850 | + }); |
| 851 | + |
| 852 | + // Verify usage was recorded (should be called for API list fetch and usage recording) |
| 853 | + const usageCalls = fetchMock.mock.calls.filter((call: [string, any]) => |
| 854 | + call[0].includes("/mcp/usage"), |
| 855 | + ); |
| 856 | + expect(usageCalls.length).toBe(1); |
| 857 | + |
| 858 | + // Verify the usage data |
| 859 | + const usageCall = usageCalls[0]; |
| 860 | + const usageBody = JSON.parse(usageCall[1].body); |
| 861 | + expect(usageBody.tool).toBe("validate_graphql"); |
| 862 | + expect(usageBody.parameters).toBe("1 code snippets"); |
| 863 | + }); |
| 864 | +}); |
0 commit comments