|
|
|
|
@ -1,8 +1,30 @@
|
|
|
|
|
import { Product } from "./Product";
|
|
|
|
|
import { MockSpreadsheetService } from "./services/MockSpreadsheetService";
|
|
|
|
|
import { MockShop } from "./test/MockShop";
|
|
|
|
|
|
|
|
|
|
// Mock Config class to avoid GAS usage
|
|
|
|
|
jest.mock("./config", () => {
|
|
|
|
|
return {
|
|
|
|
|
Config: jest.fn().mockImplementation(() => {
|
|
|
|
|
return {
|
|
|
|
|
productPhotosFolderId: "mock-folder-id",
|
|
|
|
|
shopifyApiKey: "mock-key",
|
|
|
|
|
shopifyApiSecretKey: "mock-secret",
|
|
|
|
|
shopifyAdminApiAccessToken: "mock-token",
|
|
|
|
|
shopifyApiURI: "mock-uri",
|
|
|
|
|
shopifyStorePublicationId: "mock-pub-id",
|
|
|
|
|
shopifyLocationId: "mock-loc-id",
|
|
|
|
|
shopifyCountryCodeOfOrigin: "US",
|
|
|
|
|
shopifyProvinceCodeOfOrigin: "CA",
|
|
|
|
|
salesSyncFrequency: 10
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Product", () => {
|
|
|
|
|
let mockService: MockSpreadsheetService;
|
|
|
|
|
let mockShop: MockShop;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
// Setup mock data
|
|
|
|
|
@ -16,6 +38,7 @@ describe("Product", () => {
|
|
|
|
|
productData
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
mockShop = new MockShop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should load data from inventory sheet using service", () => {
|
|
|
|
|
@ -32,4 +55,167 @@ describe("Product", () => {
|
|
|
|
|
new Product("NON-EXISTENT-SKU", mockService);
|
|
|
|
|
}).toThrow("product sku 'NON-EXISTENT-SKU' not found in product_inventory");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should skip placeholder values (?)", () => {
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "price"],
|
|
|
|
|
["TEST-SKU-2", "Original Title", 20.00]
|
|
|
|
|
]);
|
|
|
|
|
const product = new Product("TEST-SKU-2", mockService);
|
|
|
|
|
|
|
|
|
|
// Simulate edit with placeholder
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "price"],
|
|
|
|
|
["TEST-SKU-2", "?", 25.00]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Create new instance to trigger import
|
|
|
|
|
const updatedProduct = new Product("TEST-SKU-2", mockService);
|
|
|
|
|
|
|
|
|
|
// Title should be empty (default) because (?) means skip,
|
|
|
|
|
// BUT Product.ts logic says: if value is "?", continue.
|
|
|
|
|
// The Product class properties are initialized to defaults (e.g. title="").
|
|
|
|
|
// If import finds "?", it continues loop, so title remains "".
|
|
|
|
|
expect(updatedProduct.title).toBe("");
|
|
|
|
|
expect(updatedProduct.price).toBe(25.00);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should ignore properties not in class", () => {
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "unknown_col"],
|
|
|
|
|
["TEST-SKU-3", "Title 3", "some value"]
|
|
|
|
|
]);
|
|
|
|
|
const product = new Product("TEST-SKU-3", mockService);
|
|
|
|
|
expect((product as any).unknown_col).toBeUndefined();
|
|
|
|
|
expect(product.title).toBe("Title 3");
|
|
|
|
|
});
|
|
|
|
|
it("should map fields correctly to ShopifyProductSet", () => {
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "tags", "description", "product_type", "price", "compare_at_price"],
|
|
|
|
|
["TEST-SKU-4", "Mapper Test", "tag1, tag2", "<p>Desc</p>", "Type B", 50.00, 60.00]
|
|
|
|
|
]);
|
|
|
|
|
mockService.setSheetData("values", [
|
|
|
|
|
["product_type", "shopify_category", "ebay_category_id"],
|
|
|
|
|
["Type B", "Shopify Cat B", "Ebay Cat B"]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const product = new Product("TEST-SKU-4", mockService);
|
|
|
|
|
product.shopify_status = "ACTIVE"; // Simulate status set
|
|
|
|
|
|
|
|
|
|
const sps = product.ToShopifyProductSet();
|
|
|
|
|
|
|
|
|
|
expect(sps.title).toBe("Mapper Test");
|
|
|
|
|
expect(sps.tags).toBe("tag1, tag2");
|
|
|
|
|
expect(sps.descriptionHtml).toBe("<p>Desc</p>");
|
|
|
|
|
expect(sps.productType).toBe("Type B");
|
|
|
|
|
expect(sps.category).toBe("Shopify Cat B");
|
|
|
|
|
expect(sps.status).toBe("ACTIVE");
|
|
|
|
|
expect((sps as any).handle).toBe("TEST-SKU-4");
|
|
|
|
|
|
|
|
|
|
expect(sps.variants[0].price).toBe(50.00);
|
|
|
|
|
expect(sps.variants[0].compareAtPrice).toBe(60.00);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should create new product in Shopify if ID not present", () => {
|
|
|
|
|
// Setup data
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "weight_grams", "shopify_id", "product_type", "photos"],
|
|
|
|
|
["TEST-NEW-1", "New Prod", "500", "", "Type A", ""]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Mock Config
|
|
|
|
|
// Note: Config is instantiated inside Product.ts using new Config().
|
|
|
|
|
// Since we cannot mock the constructor easily without DI, and Product.ts imports Config directly,
|
|
|
|
|
// we rely on the fact that Config reads from sheet "vars".
|
|
|
|
|
mockService.setSheetData("vars", [
|
|
|
|
|
["key", "value"],
|
|
|
|
|
["shopifyLocationId", "loc_123"],
|
|
|
|
|
["shopifyCountryCodeOfOrigin", "US"],
|
|
|
|
|
["shopifyProvinceCodeOfOrigin", "CA"]
|
|
|
|
|
]);
|
|
|
|
|
mockService.setSheetData("values", [
|
|
|
|
|
["product_type", "shopify_category", "ebay_category_id"],
|
|
|
|
|
["Type A", "Category A", "123"]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const product = new Product("TEST-NEW-1", mockService);
|
|
|
|
|
|
|
|
|
|
// Mock Shop responses
|
|
|
|
|
// 1. MatchToShopifyProduct (GetProductBySku) -> returns empty/null
|
|
|
|
|
mockShop.mockProductBySku = {};
|
|
|
|
|
|
|
|
|
|
// 2. productSet mutation
|
|
|
|
|
// already mocked in MockShop.shopifyGraphQLAPI to return { product: { id: "mock-new-id" } }
|
|
|
|
|
|
|
|
|
|
// 3. GetInventoryItemBySku loop
|
|
|
|
|
// Needs to return item with ID.
|
|
|
|
|
// MockShop.GetInventoryItemBySku returns { id: "mock-inv-id" }
|
|
|
|
|
|
|
|
|
|
product.UpdateShopifyProduct(mockShop);
|
|
|
|
|
|
|
|
|
|
// Verify interactions
|
|
|
|
|
// 1. Match called (GetProductBySku)
|
|
|
|
|
expect(mockShop.getProductBySkuCalledWith).toBe("TEST-NEW-1");
|
|
|
|
|
|
|
|
|
|
// 2. productSet called
|
|
|
|
|
expect(mockShop.productSetCalledWith).toBeTruthy();
|
|
|
|
|
const sps = mockShop.productSetCalledWith.variables.productSet;
|
|
|
|
|
expect(sps.title).toBe("New Prod");
|
|
|
|
|
expect(sps.handle).toBe("TEST-NEW-1");
|
|
|
|
|
|
|
|
|
|
// 3. shopify_id updated in object and sheet
|
|
|
|
|
expect(product.shopify_id).toBe("mock-new-id");
|
|
|
|
|
const updatedId = mockService.getCellValueByColumnName("product_inventory", 2, "shopify_id");
|
|
|
|
|
expect(updatedId).toBe("mock-new-id");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should update existing product using ID", () => {
|
|
|
|
|
// Setup data with ID
|
|
|
|
|
mockService.setSheetData("product_inventory", [
|
|
|
|
|
["sku", "title", "weight_grams", "shopify_id", "product_type", "photos"],
|
|
|
|
|
["TEST-EXISTING-1", "Updated Title", "1000", "123456", "Type A", ""]
|
|
|
|
|
]);
|
|
|
|
|
mockService.setSheetData("vars", [
|
|
|
|
|
["key", "value"],
|
|
|
|
|
["shopifyLocationId", "loc_123"],
|
|
|
|
|
["shopifyCountryCodeOfOrigin", "US"],
|
|
|
|
|
["shopifyProvinceCodeOfOrigin", "CA"]
|
|
|
|
|
]);
|
|
|
|
|
mockService.setSheetData("values", [
|
|
|
|
|
["product_type", "shopify_category", "ebay_category_id"],
|
|
|
|
|
["Type A", "Category A", "123"]
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const product = new Product("TEST-EXISTING-1", mockService);
|
|
|
|
|
|
|
|
|
|
// Mock Shop Match
|
|
|
|
|
// MatchToShopifyProduct called -> check ID -> GetProductById
|
|
|
|
|
mockShop.mockProductById = {
|
|
|
|
|
id: "123456",
|
|
|
|
|
title: "Old Title",
|
|
|
|
|
variants: { nodes: [{ id: "gid://shopify/ProductVariant/123456", sku: "TEST-EXISTING-1" }] },
|
|
|
|
|
options: [{ id: "opt1", optionValues: [{ id: "optval1" }] }]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Mock Shop Update
|
|
|
|
|
// productSet mutation
|
|
|
|
|
mockShop.mockResponse = {
|
|
|
|
|
productSet: {
|
|
|
|
|
product: { id: "123456" }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// GetInventoryItem -> returns item
|
|
|
|
|
mockShop.setInventoryItemCalledWith = null; // spy
|
|
|
|
|
|
|
|
|
|
product.UpdateShopifyProduct(mockShop);
|
|
|
|
|
|
|
|
|
|
// Verify Match Used ID
|
|
|
|
|
expect(mockShop.getProductByIdCalledWith).toBe("123456");
|
|
|
|
|
|
|
|
|
|
// Verify Update Payload
|
|
|
|
|
expect(mockShop.productSetCalledWith).toBeTruthy();
|
|
|
|
|
const sps = mockShop.productSetCalledWith.variables.productSet;
|
|
|
|
|
expect(sps.title).toBe("Updated Title"); // Should use new title from sheet
|
|
|
|
|
expect(sps.id).toBe("123456"); // Should include ID to update
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|