693 lines
17 KiB
JavaScript
693 lines
17 KiB
JavaScript
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");
|
|
});
|
|
});
|
|
});
|