From 9303aab78e4107a48b431001db6a274dfd141f3f Mon Sep 17 00:00:00 2001 From: Mathieu Date: Mon, 8 Jun 2026 23:21:18 +0200 Subject: [PATCH] feat(plan4): API GET /api/products/list avec filtre statut (TDD) --- src/app/api/products/list/route.test.ts | 59 +++++++++++++++++++++++++ src/app/api/products/list/route.ts | 33 ++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/app/api/products/list/route.test.ts create mode 100644 src/app/api/products/list/route.ts diff --git a/src/app/api/products/list/route.test.ts b/src/app/api/products/list/route.test.ts new file mode 100644 index 0000000..24cf0cf --- /dev/null +++ b/src/app/api/products/list/route.test.ts @@ -0,0 +1,59 @@ +import { GET } from './route' +import * as shopifySync from '@/lib/shopifySync' +import type { ShopifyProductFull } from '@/types/sync' + +jest.mock('@/lib/shopifySync') +const mockFetch = shopifySync.fetchAllShopifyProducts as jest.MockedFunction + +const PRODUCTS: ShopifyProductFull[] = [ + { shopifyId: '1', ref: 'REF-001', handle: 'ref-001', title: 'Brique rouge', description: '', price: '12.50', stock: 100, status: 'active' }, + { shopifyId: '2', ref: 'REF-002', handle: 'ref-002', title: 'Parpaing', description: '', price: '5.00', stock: 0, status: 'draft' }, + { shopifyId: '3', ref: 'REF-003', handle: 'ref-003', title: 'Ardoise', description: '', price: '80.00', stock: 20, status: 'active' }, +] + +describe('GET /api/products/list', () => { + beforeEach(() => { + process.env.SHOPIFY_STORE_DOMAIN = 'test.myshopify.com' + process.env.SHOPIFY_ADMIN_API_TOKEN = 'token' + mockFetch.mockResolvedValue(PRODUCTS) + }) + afterEach(() => jest.clearAllMocks()) + + it('retourne tous les produits sans filtre', async () => { + const req = new Request('http://localhost/api/products/list') + const res = await GET(req as never) + const body = await res.json() + expect(body.total).toBe(3) + expect(body.products).toHaveLength(3) + }) + + it('filtre par status=active', async () => { + const req = new Request('http://localhost/api/products/list?status=active') + const res = await GET(req as never) + const body = await res.json() + expect(body.total).toBe(2) + body.products.forEach((p: { status: string }) => expect(p.status).toBe('active')) + }) + + it('filtre par status=draft', async () => { + const req = new Request('http://localhost/api/products/list?status=draft') + const res = await GET(req as never) + const body = await res.json() + expect(body.total).toBe(1) + expect(body.products[0].ref).toBe('REF-002') + }) + + it('retourne shopifyUrl pour chaque produit', async () => { + const req = new Request('http://localhost/api/products/list') + const res = await GET(req as never) + const body = await res.json() + expect(body.products[0].shopifyUrl).toContain('test.myshopify.com/admin/products/1') + }) + + it('retourne 500 si Shopify inaccessible', async () => { + mockFetch.mockRejectedValue(new Error('network error')) + const req = new Request('http://localhost/api/products/list') + const res = await GET(req as never) + expect(res.status).toBe(500) + }) +}) diff --git a/src/app/api/products/list/route.ts b/src/app/api/products/list/route.ts new file mode 100644 index 0000000..aab5eec --- /dev/null +++ b/src/app/api/products/list/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server' +import { fetchAllShopifyProducts } from '@/lib/shopifySync' +import type { CatalogProduct, ProductListResponse } from '@/types/products' + +export async function GET(req: NextRequest): Promise> { + try { + const { searchParams } = new URL(req.url) + const statusFilter = searchParams.get('status') ?? 'all' + + const domain = process.env.SHOPIFY_STORE_DOMAIN ?? '' + const shopifyProducts = await fetchAllShopifyProducts() + + let products: CatalogProduct[] = shopifyProducts.map((p) => ({ + shopifyId: p.shopifyId, + ref: p.ref, + handle: p.handle, + title: p.title, + price: p.price, + stock: p.stock, + status: p.status as CatalogProduct['status'], + shopifyUrl: `https://${domain}/admin/products/${p.shopifyId}`, + })) + + if (statusFilter !== 'all') { + products = products.filter((p) => p.status === statusFilter) + } + + return NextResponse.json({ products, total: products.length }) + } catch (err) { + const message = err instanceof Error ? err.message : 'Erreur inconnue' + return NextResponse.json({ error: message }, { status: 500 }) + } +}