const TagAnalysisService = require("../../src/services/TagAnalysisService"); const ShopifyService = require("../../src/services/shopify"); // Mock the ShopifyService jest.mock("../../src/services/shopify"); describe("TagAnalysisService", () => { let tagAnalysisService; let mockShopifyService; beforeEach(() => { // Clear all mocks jest.clearAllMocks(); // Create mock ShopifyService instance mockShopifyService = { executeWithRetry: jest.fn(), executeQuery: jest.fn(), }; // Mock the ShopifyService constructor ShopifyService.mockImplementation(() => mockShopifyService); tagAnalysisService = new TagAnalysisService(); }); describe("constructor", () => { it("should initialize with ShopifyService", () => { expect(ShopifyService).toHaveBeenCalledTimes(1); expect(tagAnalysisService.pageSize).toBe(50); }); }); describe("fetchAllTags", () => { it("should fetch all tags successfully with single page", async () => { const mockResponse = { products: { edges: [ { node: { id: "product1", title: "Product 1", tags: ["tag1", "tag2"], variants: { edges: [ { node: { id: "variant1", price: "10.00", title: "Variant 1", }, }, { node: { id: "variant2", price: "20.00", title: "Variant 2", }, }, ], }, }, }, { node: { id: "product2", title: "Product 2", tags: ["tag1", "tag3"], variants: { edges: [ { node: { id: "variant3", price: "15.00", title: "Variant 3", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const result = await tagAnalysisService.fetchAllTags(); expect(result).toHaveLength(3); expect(result[0].tag).toBe("tag1"); expect(result[0].productCount).toBe(2); expect(result[0].variantCount).toBe(3); expect(result[0].totalValue).toBe(45); // 10 + 20 + 15 expect(result[0].averagePrice).toBe(15); expect(result[1].tag).toBe("tag2"); expect(result[1].productCount).toBe(1); expect(result[1].variantCount).toBe(2); expect(result[1].totalValue).toBe(30); // 10 + 20 expect(result[2].tag).toBe("tag3"); expect(result[2].productCount).toBe(1); expect(result[2].variantCount).toBe(1); expect(result[2].totalValue).toBe(15); }); it("should handle multiple pages", async () => { const mockResponse1 = { products: { edges: [ { node: { id: "product1", title: "Product 1", tags: ["tag1"], variants: { edges: [ { node: { id: "variant1", price: "10.00", title: "Variant 1", }, }, ], }, }, }, ], pageInfo: { hasNextPage: true, endCursor: "cursor1", }, }, }; const mockResponse2 = { products: { edges: [ { node: { id: "product2", title: "Product 2", tags: ["tag2"], variants: { edges: [ { node: { id: "variant2", price: "20.00", title: "Variant 2", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); const result = await tagAnalysisService.fetchAllTags(); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(2); expect(result).toHaveLength(2); expect(result[0].tag).toBe("tag1"); expect(result[1].tag).toBe("tag2"); }); it("should handle products with no tags", async () => { const mockResponse = { products: { edges: [ { node: { id: "product1", title: "Product 1", tags: [], variants: { edges: [ { node: { id: "variant1", price: "10.00", title: "Variant 1", }, }, ], }, }, }, { node: { id: "product2", title: "Product 2", tags: null, variants: { edges: [ { node: { id: "variant2", price: "20.00", title: "Variant 2", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const result = await tagAnalysisService.fetchAllTags(); expect(result).toHaveLength(0); }); it("should handle API errors", async () => { const mockError = new Error("API connection failed"); mockShopifyService.executeWithRetry.mockRejectedValue(mockError); await expect(tagAnalysisService.fetchAllTags()).rejects.toThrow( "Tag fetching failed: API connection failed" ); }); it("should handle invalid response structure", async () => { const mockResponse = { // Missing products field data: {}, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); await expect(tagAnalysisService.fetchAllTags()).rejects.toThrow( "Invalid response structure: missing products field" ); }); }); describe("getTagDetails", () => { it("should get detailed tag information", async () => { const mockResponse = { products: { edges: [ { node: { id: "product1", title: "Product 1", tags: ["test-tag", "other-tag"], variants: { edges: [ { node: { id: "variant1", price: "10.00", compareAtPrice: "12.00", title: "Variant 1", }, }, { node: { id: "variant2", price: "20.00", compareAtPrice: null, title: "Variant 2", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); const result = await tagAnalysisService.getTagDetails("test-tag"); expect(result.tag).toBe("test-tag"); expect(result.productCount).toBe(1); expect(result.variantCount).toBe(2); expect(result.totalValue).toBe(30); expect(result.averagePrice).toBe(15); expect(result.priceRange.min).toBe(10); expect(result.priceRange.max).toBe(20); expect(result.products).toHaveLength(1); expect(result.products[0].title).toBe("Product 1"); expect(result.products[0].variants).toHaveLength(2); }); it("should handle tag with 'tag:' prefix", async () => { const mockResponse = { products: { edges: [], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry.mockResolvedValue(mockResponse); await tagAnalysisService.getTagDetails("tag:test-tag"); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledWith( expect.any(Function) ); // Verify the query was called with the correct tag format const callArgs = mockShopifyService.executeWithRetry.mock.calls[0]; const queryFunction = callArgs[0]; // Mock the executeQuery to capture the variables mockShopifyService.executeQuery.mockResolvedValue(mockResponse); await queryFunction(); expect(mockShopifyService.executeQuery).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ query: "tag:test-tag", }) ); }); it("should handle multiple pages for tag details", async () => { const mockResponse1 = { products: { edges: [ { node: { id: "product1", title: "Product 1", tags: ["test-tag"], variants: { edges: [ { node: { id: "variant1", price: "10.00", compareAtPrice: null, title: "Variant 1", }, }, ], }, }, }, ], pageInfo: { hasNextPage: true, endCursor: "cursor1", }, }, }; const mockResponse2 = { products: { edges: [ { node: { id: "product2", title: "Product 2", tags: ["test-tag"], variants: { edges: [ { node: { id: "variant2", price: "20.00", compareAtPrice: null, title: "Variant 2", }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, endCursor: null, }, }, }; mockShopifyService.executeWithRetry .mockResolvedValueOnce(mockResponse1) .mockResolvedValueOnce(mockResponse2); const result = await tagAnalysisService.getTagDetails("test-tag"); expect(mockShopifyService.executeWithRetry).toHaveBeenCalledTimes(2); expect(result.products).toHaveLength(2); expect(result.productCount).toBe(2); expect(result.variantCount).toBe(2); }); it("should handle API errors in getTagDetails", async () => { const mockError = new Error("Network error"); mockShopifyService.executeWithRetry.mockRejectedValue(mockError); await expect( tagAnalysisService.getTagDetails("test-tag") ).rejects.toThrow("Tag analysis failed: Network error"); }); }); describe("calculateTagStatistics", () => { it("should calculate statistics correctly", () => { const products = [ { id: "product1", title: "Product 1", variants: [ { id: "variant1", price: 10, title: "Variant 1" }, { id: "variant2", price: 20, title: "Variant 2" }, ], }, { id: "product2", title: "Product 2", variants: [{ id: "variant3", price: 15, title: "Variant 3" }], }, ]; const result = tagAnalysisService.calculateTagStatistics(products); expect(result.productCount).toBe(2); expect(result.variantCount).toBe(3); expect(result.totalValue).toBe(45); expect(result.averagePrice).toBe(15); expect(result.priceRange.min).toBe(10); expect(result.priceRange.max).toBe(20); }); it("should handle empty products array", () => { const result = tagAnalysisService.calculateTagStatistics([]); expect(result.productCount).toBe(0); expect(result.variantCount).toBe(0); expect(result.totalValue).toBe(0); expect(result.averagePrice).toBe(0); expect(result.priceRange.min).toBe(0); expect(result.priceRange.max).toBe(0); }); it("should handle null/undefined products", () => { const result1 = tagAnalysisService.calculateTagStatistics(null); const result2 = tagAnalysisService.calculateTagStatistics(undefined); expect(result1.productCount).toBe(0); expect(result2.productCount).toBe(0); }); it("should handle products with invalid prices", () => { const products = [ { id: "product1", title: "Product 1", variants: [ { id: "variant1", price: 10, title: "Variant 1" }, { id: "variant2", price: NaN, title: "Variant 2" }, { id: "variant3", price: "invalid", title: "Variant 3" }, ], }, ]; const result = tagAnalysisService.calculateTagStatistics(products); expect(result.productCount).toBe(1); expect(result.variantCount).toBe(1); // Only valid price counted expect(result.totalValue).toBe(10); expect(result.averagePrice).toBe(10); }); it("should handle products with no variants", () => { const products = [ { id: "product1", title: "Product 1", variants: [], }, { id: "product2", title: "Product 2", variants: null, }, ]; const result = tagAnalysisService.calculateTagStatistics(products); expect(result.productCount).toBe(2); expect(result.variantCount).toBe(0); expect(result.totalValue).toBe(0); expect(result.averagePrice).toBe(0); }); }); describe("searchTags", () => { const mockTags = [ { tag: "summer-sale", productCount: 5, products: [ { id: "1", title: "Summer Dress", variantCount: 2 }, { id: "2", title: "Beach Hat", variantCount: 1 }, ], }, { tag: "winter-collection", productCount: 3, products: [{ id: "3", title: "Winter Coat", variantCount: 3 }], }, { tag: "accessories", productCount: 8, products: [{ id: "4", title: "Summer Sunglasses", variantCount: 1 }], }, ]; it("should return all tags when query is empty", () => { const result1 = tagAnalysisService.searchTags(mockTags, ""); const result2 = tagAnalysisService.searchTags(mockTags, " "); const result3 = tagAnalysisService.searchTags(mockTags, null); const result4 = tagAnalysisService.searchTags(mockTags, undefined); expect(result1).toEqual(mockTags); expect(result2).toEqual(mockTags); expect(result3).toEqual(mockTags); expect(result4).toEqual(mockTags); }); it("should filter tags by tag name", () => { const result = tagAnalysisService.searchTags(mockTags, "summer-sale"); expect(result).toHaveLength(1); expect(result[0].tag).toBe("summer-sale"); }); it("should filter tags by product title", () => { const result = tagAnalysisService.searchTags(mockTags, "coat"); expect(result).toHaveLength(1); expect(result[0].tag).toBe("winter-collection"); }); it("should be case insensitive", () => { const result1 = tagAnalysisService.searchTags(mockTags, "SUMMER-SALE"); const result2 = tagAnalysisService.searchTags(mockTags, "Winter"); expect(result1).toHaveLength(1); expect(result1[0].tag).toBe("summer-sale"); expect(result2).toHaveLength(1); expect(result2[0].tag).toBe("winter-collection"); }); it("should return multiple matches", () => { const result = tagAnalysisService.searchTags(mockTags, "summer"); // Should match both "summer-sale" tag and "Summer Sunglasses" product expect(result).toHaveLength(2); expect(result.map((t) => t.tag)).toContain("summer-sale"); expect(result.map((t) => t.tag)).toContain("accessories"); }); it("should return empty array when no matches found", () => { const result = tagAnalysisService.searchTags(mockTags, "nonexistent"); expect(result).toHaveLength(0); }); it("should handle tags without products array", () => { const tagsWithoutProducts = [ { tag: "test-tag", productCount: 1, // No products array }, ]; const result = tagAnalysisService.searchTags(tagsWithoutProducts, "test"); expect(result).toHaveLength(1); expect(result[0].tag).toBe("test-tag"); }); }); describe("getTagAnalysisSummary", () => { it("should calculate summary statistics correctly", () => { const tags = [ { tag: "tag1", productCount: 5, variantCount: 10, totalValue: 100, }, { tag: "tag2", productCount: 3, variantCount: 6, totalValue: 60, }, { tag: "tag3", productCount: 2, variantCount: 4, totalValue: 40, }, ]; const result = tagAnalysisService.getTagAnalysisSummary(tags); expect(result.totalTags).toBe(3); expect(result.totalProducts).toBe(10); expect(result.totalVariants).toBe(20); expect(result.totalValue).toBe(200); expect(result.averageProductsPerTag).toBe(10 / 3); expect(result.averageVariantsPerTag).toBe(20 / 3); }); it("should handle empty tags array", () => { const result = tagAnalysisService.getTagAnalysisSummary([]); expect(result.totalTags).toBe(0); expect(result.totalProducts).toBe(0); expect(result.totalVariants).toBe(0); expect(result.totalValue).toBe(0); expect(result.averageProductsPerTag).toBe(0); expect(result.averageVariantsPerTag).toBe(0); }); it("should handle null/undefined tags", () => { const result1 = tagAnalysisService.getTagAnalysisSummary(null); const result2 = tagAnalysisService.getTagAnalysisSummary(undefined); expect(result1.totalTags).toBe(0); expect(result2.totalTags).toBe(0); }); }); describe("GraphQL queries", () => { it("should have correct getAllProductsWithTagsQuery structure", () => { const query = tagAnalysisService.getAllProductsWithTagsQuery(); expect(query).toContain("query getAllProductsWithTags"); expect(query).toContain("products(first: $first, after: $after)"); expect(query).toContain("tags"); expect(query).toContain("variants"); expect(query).toContain("pageInfo"); }); it("should have correct getProductsByTagQuery structure", () => { const query = tagAnalysisService.getProductsByTagQuery(); expect(query).toContain("query getProductsByTag"); expect(query).toContain( "products(first: $first, after: $after, query: $query)" ); expect(query).toContain("tags"); expect(query).toContain("variants"); expect(query).toContain("compareAtPrice"); expect(query).toContain("pageInfo"); }); }); });