/**
* Integration tests for the Shopify API layer.
* Uses MSW (Mock Service Worker) to intercept and mock Shopify GraphQL endpoints.
*/
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { ShopifyClient } from '../../src/shopify/client.js';
import { StorefrontAPI } from '../../src/shopify/storefront.js';
import { AdminAPI } from '../../src/shopify/admin.js';
// ─── Test Configuration ───
const clientConfig = {
storeDomain: 'test-store.myshopify.com',
accessToken: 'test-access-token',
storefrontToken: 'test-storefront-token',
};
const STOREFRONT_URL = `https://${clientConfig.storeDomain}/api/2025-01/graphql.json`;
const ADMIN_URL = `https://${clientConfig.storeDomain}/admin/api/2025-01/graphql.json`;
// ─── Mock Data ───
const mockShopifyProduct = {
id: 'gid://shopify/Product/123456',
title: 'Test Sneakers',
description: 'A comfortable pair of test sneakers.',
vendor: 'TestBrand',
productType: 'Shoes',
tags: ['sneakers', 'sale'],
availableForSale: true,
createdAt: '2025-06-01T00:00:00Z',
updatedAt: '2025-07-15T12:00:00Z',
variants: {
edges: [
{
node: {
id: 'gid://shopify/ProductVariant/111',
title: 'Size 10 / Black',
sku: 'SNKR-BLK-10',
price: { amount: '89.99', currencyCode: 'USD' },
availableForSale: true,
quantityAvailable: 25,
barcode: '1234567890123',
},
},
],
},
images: {
edges: [
{
node: {
url: 'https://cdn.shopify.com/test-sneakers.jpg',
altText: 'Black sneakers side view',
width: 800,
height: 600,
},
},
],
},
};
const mockShopifyOrder = {
id: 'gid://shopify/Order/900001',
name: '#1001',
displayFinancialStatus: 'PAID',
displayFulfillmentStatus: 'FULFILLED',
currencyCode: 'USD',
createdAt: '2025-08-01T10:00:00Z',
updatedAt: '2025-08-03T14:30:00Z',
totalPriceSet: { shopMoney: { amount: '99.99', currencyCode: 'USD' } },
subtotalPriceSet: { shopMoney: { amount: '89.99', currencyCode: 'USD' } },
totalTaxSet: { shopMoney: { amount: '5.00', currencyCode: 'USD' } },
totalShippingPriceSet: { shopMoney: { amount: '5.00', currencyCode: 'USD' } },
totalDiscountsSet: { shopMoney: { amount: '0.00', currencyCode: 'USD' } },
lineItems: {
edges: [
{
node: {
id: 'gid://shopify/LineItem/500001',
title: 'Test Sneakers - Size 10 / Black',
quantity: 1,
sku: 'SNKR-BLK-10',
variant: {
id: 'gid://shopify/ProductVariant/111',
product: { id: 'gid://shopify/Product/123456' },
image: {
url: 'https://cdn.shopify.com/test-sneakers.jpg',
altText: 'Black sneakers',
width: 800,
height: 600,
},
price: '89.99',
},
originalTotalSet: { shopMoney: { amount: '89.99', currencyCode: 'USD' } },
discountedTotalSet: { shopMoney: { amount: '89.99', currencyCode: 'USD' } },
},
},
],
},
fulfillments: [
{
status: 'SUCCESS',
trackingInfo: [
{
number: '1Z999AA10123456784',
url: 'https://tracking.example.com/1Z999AA10123456784',
company: 'UPS',
},
],
estimatedDeliveryAt: '2025-08-05T00:00:00Z',
},
],
};
const mockDiscountCodeNode = {
id: 'gid://shopify/DiscountCodeNode/700001',
codeDiscount: {
title: 'Summer Sale 20%',
summary: '20% off your entire order',
codes: {
edges: [{ node: { code: 'SUMMER20' } }],
},
customerGets: {
value: { percentage: 0.2 },
},
minimumRequirement: {
greaterThanOrEqualToSubtotal: { amount: '50.00', currencyCode: 'USD' },
},
},
};
// ─── Default MSW Handlers ───
const handlers = [
// Storefront API handler
http.post(STOREFRONT_URL, () => {
return HttpResponse.json({
data: {
products: {
edges: [{ node: mockShopifyProduct }],
},
},
});
}),
// Admin API handler
http.post(ADMIN_URL, () => {
return HttpResponse.json({
data: {
order: mockShopifyOrder,
},
});
}),
];
// ─── Server Setup ───
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// ─── Tests ───
describe('StorefrontAPI.searchProducts', () => {
it('returns CatalogProduct array from search results', async () => {
server.use(
http.post(STOREFRONT_URL, () => {
return HttpResponse.json({
data: {
products: {
edges: [{ node: mockShopifyProduct }],
},
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const storefront = new StorefrontAPI(client);
const products = await storefront.searchProducts('sneakers', 5);
expect(products).toHaveLength(1);
const product = products[0]!;
expect(product.id).toBe('gid://shopify/Product/123456');
expect(product.title).toBe('Test Sneakers');
expect(product.description).toBe('A comfortable pair of test sneakers.');
expect(product.vendor).toBe('TestBrand');
expect(product.product_type).toBe('Shoes');
expect(product.tags).toEqual(['sneakers', 'sale']);
expect(product.available).toBe(true);
// Variants
expect(product.variants).toHaveLength(1);
const variant = product.variants[0]!;
expect(variant.id).toBe('gid://shopify/ProductVariant/111');
expect(variant.title).toBe('Size 10 / Black');
expect(variant.sku).toBe('SNKR-BLK-10');
expect(variant.price).toBe(8999); // $89.99 in cents
expect(variant.currency).toBe('USD');
expect(variant.available).toBe(true);
expect(variant.inventory_quantity).toBe(25);
expect(variant.gtin).toBe('1234567890123');
// Images
expect(product.images).toHaveLength(1);
const image = product.images[0]!;
expect(image.url).toBe('https://cdn.shopify.com/test-sneakers.jpg');
expect(image.alt_text).toBe('Black sneakers side view');
expect(image.width).toBe(800);
expect(image.height).toBe(600);
});
});
describe('StorefrontAPI.getProduct', () => {
it('returns a CatalogProduct when the product exists', async () => {
server.use(
http.post(STOREFRONT_URL, () => {
return HttpResponse.json({
data: {
product: mockShopifyProduct,
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const storefront = new StorefrontAPI(client);
const product = await storefront.getProduct('gid://shopify/Product/123456');
expect(product).not.toBeNull();
expect(product!.id).toBe('gid://shopify/Product/123456');
expect(product!.title).toBe('Test Sneakers');
expect(product!.variants).toHaveLength(1);
expect(product!.images).toHaveLength(1);
});
it('returns null when the product does not exist', async () => {
server.use(
http.post(STOREFRONT_URL, () => {
return HttpResponse.json({
data: { product: null },
});
}),
);
const client = new ShopifyClient(clientConfig);
const storefront = new StorefrontAPI(client);
const product = await storefront.getProduct('gid://shopify/Product/nonexistent');
expect(product).toBeNull();
});
});
describe('StorefrontAPI.createCart', () => {
it('returns the new cart ID', async () => {
server.use(
http.post(STOREFRONT_URL, () => {
return HttpResponse.json({
data: {
cartCreate: {
cart: { id: 'gid://shopify/Cart/test-1' },
userErrors: [],
},
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const storefront = new StorefrontAPI(client);
const cartId = await storefront.createCart();
expect(cartId).toBe('gid://shopify/Cart/test-1');
});
});
describe('AdminAPI.getOrder', () => {
it('returns a UCP Order when the order exists', async () => {
server.use(
http.post(ADMIN_URL, () => {
return HttpResponse.json({
data: {
order: mockShopifyOrder,
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const admin = new AdminAPI(client);
const order = await admin.getOrder('gid://shopify/Order/900001');
expect(order).not.toBeNull();
expect(order!.id).toBe('gid://shopify/Order/900001');
expect(order!.checkout_id).toBe('#1001');
expect(order!.status).toBe('delivered'); // FULFILLED => delivered
// Totals (in minor units / cents)
expect(order!.totals.subtotal).toBe(8999);
expect(order!.totals.tax).toBe(500);
expect(order!.totals.shipping).toBe(500);
expect(order!.totals.discount).toBe(0);
expect(order!.totals.total).toBe(9999);
expect(order!.totals.currency).toBe('USD');
// Fulfillment
expect(order!.fulfillment).toBeDefined();
expect(order!.fulfillment!.status).toBe('fulfilled');
expect(order!.fulfillment!.tracking_number).toBe('1Z999AA10123456784');
expect(order!.fulfillment!.tracking_url).toBe(
'https://tracking.example.com/1Z999AA10123456784',
);
expect(order!.fulfillment!.carrier).toBe('UPS');
expect(order!.fulfillment!.estimated_delivery).toBe('2025-08-05T00:00:00Z');
// Line items
expect(order!.line_items).toHaveLength(1);
const lineItem = order!.line_items[0]!;
expect(lineItem.title).toBe('Test Sneakers - Size 10 / Black');
expect(lineItem.quantity).toBe(1);
expect(lineItem.sku).toBe('SNKR-BLK-10');
});
it('returns null when the order does not exist', async () => {
server.use(
http.post(ADMIN_URL, () => {
return HttpResponse.json({
data: { order: null },
});
}),
);
const client = new ShopifyClient(clientConfig);
const admin = new AdminAPI(client);
const order = await admin.getOrder('gid://shopify/Order/nonexistent');
expect(order).toBeNull();
});
});
describe('AdminAPI.getDiscountCodes', () => {
it('returns DiscountOffer array with correct code, type, and value', async () => {
server.use(
http.post(ADMIN_URL, () => {
return HttpResponse.json({
data: {
codeDiscountNodes: {
edges: [{ node: mockDiscountCodeNode }],
},
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const admin = new AdminAPI(client);
const discounts = await admin.getDiscountCodes();
expect(discounts).toHaveLength(1);
const discount = discounts[0]!;
expect(discount.code).toBe('SUMMER20');
expect(discount.type).toBe('percentage');
expect(discount.value).toBe(20); // 0.2 * 100
expect(discount.description).toBe('20% off your entire order');
expect(discount.min_purchase).toBe(5000); // $50.00 in cents
});
});
describe('ShopifyClient rate limiting', () => {
it('retries after receiving a 429 response and returns valid data', async () => {
let requestCount = 0;
server.use(
http.post(STOREFRONT_URL, () => {
requestCount++;
if (requestCount === 1) {
return new HttpResponse(null, {
status: 429,
headers: { 'Retry-After': '0' },
});
}
return HttpResponse.json({
data: {
products: {
edges: [{ node: mockShopifyProduct }],
},
},
});
}),
);
const client = new ShopifyClient(clientConfig);
const storefront = new StorefrontAPI(client);
const products = await storefront.searchProducts('sneakers');
expect(requestCount).toBe(2);
expect(products).toHaveLength(1);
expect(products[0]!.id).toBe('gid://shopify/Product/123456');
});
});