Files
PriceUpdaterAppv2/tests/services/TagAnalysisService.test.js

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");
});
});
});