diff --git a/test-automation/TASKS.md b/test-automation/TASKS.md index 8731494..1fb324c 100644 --- a/test-automation/TASKS.md +++ b/test-automation/TASKS.md @@ -45,17 +45,18 @@ Konzept: [`docs/ui-testing-automation.md`](./docs/ui-testing-automation.md) > Alle manual-testing Issues und Feature-Stories als Playwright-Specs. -- [ ] `tests/api/masterdata/customers.spec.ts` – TC-CUS, Issue #65 -- [ ] `tests/api/masterdata/contracts.spec.ts` – TC-B2B, Issue #66 -- [ ] `tests/api/masterdata/articles.spec.ts` – TC-ART, Issue #64 -- [ ] `tests/api/auth/authorization.spec.ts` – TC-AUTH, Issue #67 -- [ ] `tests/api/production/orders.spec.ts` – US-P13–P17, Issues #38–#42 -- [ ] `tests/api/production/batches.spec.ts` – US-P09–P12, Issues #33–#36 -- [ ] `tests/api/production/recipes.spec.ts` – US-P02–P08, Issues #26–#32 -- [ ] `tests/api/production/traceability.spec.ts` – US-P18–P19, Issues #43–#44 -- [ ] `tests/api/inventory/stock.spec.ts` – Story 2.x–6.x, Issues #4–#20 -- [ ] `tests/api/inventory/movements.spec.ts` -- [ ] `tests/api/inventory/reservations.spec.ts` +- [x] `tests/api/masterdata/customers.spec.ts` – TC-CUS, Issue #65 +- [x] `tests/api/masterdata/contracts.spec.ts` – TC-B2B, Issue #66 +- [x] `tests/api/masterdata/articles.spec.ts` – TC-ART, Issue #64 +- [x] `tests/api/auth/authorization.spec.ts` – TC-AUTH, Issue #67 (erweitert auf 7 TCs) +- [x] `tests/api/production/orders.spec.ts` – US-P13–P17, Issues #38–#42 +- [x] `tests/api/production/batches.spec.ts` – US-P09–P12, Issues #33–#36 +- [x] `tests/api/production/recipes.spec.ts` – US-P02–P08, Issues #26–#32 +- [x] `tests/api/production/traceability.spec.ts` – US-P18–P19, Issues #43–#44 +- [x] `tests/api/inventory/stock.spec.ts` – Story 2.x–6.x, Issues #4–#20 +- [x] `tests/api/inventory/movements.spec.ts` +- [x] `tests/api/inventory/reservations.spec.ts` +- [x] `tests/api/inventory/inventory-counts.spec.ts` - [ ] Test-Generierungs-Skript (`scripts/generate-tests-from-issue.ts`) finalisieren - [ ] Skript in `justfile` als `just generate-test ` integrieren - [ ] Alle Specs im Docker-Stack grün diff --git a/test-automation/web-ui/tests/api/auth/authorization.spec.ts b/test-automation/web-ui/tests/api/auth/authorization.spec.ts index 07a76fd..21da24b 100644 --- a/test-automation/web-ui/tests/api/auth/authorization.spec.ts +++ b/test-automation/web-ui/tests/api/auth/authorization.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '../../../fixtures/auth.fixture.js'; /** - * TC-AUTH – Autorisierung Masterdata + * TC-AUTH – Autorisierung * Quelle: GitHub Issue #67 */ test.describe('TC-AUTH: Autorisierung', () => { @@ -26,5 +26,33 @@ test.describe('TC-AUTH: Autorisierung', () => { expect(res.status()).toBe(403); }); - // TODO: Weitere ACs aus Issue #67 ergänzen + test('TC-AUTH-04: Viewer darf Lieferanten lesen', async ({ request, viewerToken }) => { + const res = await request.get('/api/suppliers', { + headers: { Authorization: `Bearer ${viewerToken}` }, + }); + expect(res.status()).toBe(200); + }); + + test('TC-AUTH-05: Viewer darf keine Kategorien erstellen', async ({ request, viewerToken }) => { + const res = await request.post('/api/categories', { + data: { name: `Viewer-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${viewerToken}` }, + }); + expect(res.status()).toBe(403); + }); + + test('TC-AUTH-06: Admin darf Kategorien erstellen', async ({ request, adminToken }) => { + const res = await request.post('/api/categories', { + data: { name: `AdminKat-${Date.now()}` }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + }); + + test('TC-AUTH-07: Ungültiges JWT wird abgelehnt', async ({ request }) => { + const res = await request.get('/api/suppliers', { + headers: { Authorization: 'Bearer invalid.jwt.token' }, + }); + expect([401, 403]).toContain(res.status()); + }); }); diff --git a/test-automation/web-ui/tests/api/inventory/inventory-counts.spec.ts b/test-automation/web-ui/tests/api/inventory/inventory-counts.spec.ts new file mode 100644 index 0000000..49a5c7a --- /dev/null +++ b/test-automation/web-ui/tests/api/inventory/inventory-counts.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-INV – Inventurzählungen + * Quelle: GitHub Issues #4–#20 + */ +test.describe('TC-INV: Inventurzählungen', () => { + async function createStorageLocation(request: Parameters[1]['request'], token: string): Promise { + const res = await request.post('/api/inventory/storage-locations', { + data: { name: `INV-Lager-${Date.now()}`, storageType: 'DRY_STORAGE' }, + headers: { Authorization: `Bearer ${token}` }, + }); + const body = await res.json(); + return body.id; + } + + test('TC-INV-01: Inventurzählung anlegen', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + + const res = await request.post('/api/inventory/inventory-counts', { + data: { storageLocationId, countDate: '2026-04-30' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('PLANNED'); + }); + + test('TC-INV-02: Inventurzählung ohne Lagerort wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/inventory/inventory-counts', { + data: { countDate: '2026-04-30' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-INV-03: Inventurzählung starten', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + + const createRes = await request.post('/api/inventory/inventory-counts', { + data: { storageLocationId, countDate: '2026-05-01' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const startRes = await request.patch(`/api/inventory/inventory-counts/${id}/start`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(startRes.status()).toBe(200); + const body = await startRes.json(); + expect(body.status).toBe('IN_PROGRESS'); + }); + + test('TC-INV-04: Inventurzählung stornieren', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + + const createRes = await request.post('/api/inventory/inventory-counts', { + data: { storageLocationId, countDate: '2026-05-02' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const cancelRes = await request.post(`/api/inventory/inventory-counts/${id}/cancel`, { + data: { reason: 'Test-Stornierung' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(cancelRes.status()).toBe(200); + const body = await cancelRes.json(); + expect(body.status).toBe('CANCELLED'); + }); +}); diff --git a/test-automation/web-ui/tests/api/inventory/movements.spec.ts b/test-automation/web-ui/tests/api/inventory/movements.spec.ts new file mode 100644 index 0000000..62a4bd6 --- /dev/null +++ b/test-automation/web-ui/tests/api/inventory/movements.spec.ts @@ -0,0 +1,99 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-MOV – Warenbewegungen + * Quelle: GitHub Issues #4–#20 + */ +test.describe('TC-MOV: Warenbewegungen', () => { + async function setupStockWithBatch( + request: Parameters[1]['request'], + token: string, + ): Promise<{ stockId: string; stockBatchId: string; articleId: string; batchId: string }> { + const catRes = await request.post('/api/categories', { + data: { name: `MOV-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + + const artRes = await request.post('/api/articles', { + data: { + name: `MOV-Art-${Date.now()}`, + articleNumber: `MOV-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 1.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: articleId } = await artRes.json(); + + const locRes = await request.post('/api/inventory/storage-locations', { + data: { name: `MOV-Lager-${Date.now()}`, storageType: 'DRY_STORAGE' }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: storageLocationId } = await locRes.json(); + + const stockRes = await request.post('/api/inventory/stocks', { + data: { articleId, storageLocationId }, + headers: { Authorization: `Bearer ${token}` }, + }); + expect(stockRes.status()).toBe(201); + const { id: stockId } = await stockRes.json(); + + const batchId = `B-${Date.now()}`; + const batchRes = await request.post(`/api/inventory/stocks/${stockId}/batches`, { + data: { + batchId, + batchType: 'PURCHASED', + quantityAmount: '20', + quantityUnit: 'KG', + expiryDate: '2026-12-31', + }, + headers: { Authorization: `Bearer ${token}` }, + }); + expect(batchRes.status()).toBe(201); + const { id: stockBatchId } = await batchRes.json(); + + return { stockId, stockBatchId, articleId, batchId }; + } + + test('TC-MOV-01: Warenbewegung erfassen – Wareneingang', async ({ request, adminToken }) => { + const { stockId, stockBatchId, articleId, batchId } = await setupStockWithBatch(request, adminToken); + + const res = await request.post('/api/inventory/stock-movements', { + data: { + stockId, + articleId, + stockBatchId, + batchId, + batchType: 'PURCHASED', + movementType: 'GOODS_RECEIPT', + quantityAmount: '5', + quantityUnit: 'KG', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.movementType).toBe('GOODS_RECEIPT'); + }); + + test('TC-MOV-02: Warenbewegung ohne Pflichtfelder wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/inventory/stock-movements', { + data: { movementType: 'GOODS_RECEIPT' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-MOV-03: Warenbewegungen nach Stock auflisten', async ({ request, adminToken }) => { + const { stockId } = await setupStockWithBatch(request, adminToken); + + const res = await request.get(`/api/inventory/stock-movements?stockId=${stockId}`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + expect(Array.isArray(await res.json())).toBe(true); + }); +}); diff --git a/test-automation/web-ui/tests/api/inventory/reservations.spec.ts b/test-automation/web-ui/tests/api/inventory/reservations.spec.ts new file mode 100644 index 0000000..04e2181 --- /dev/null +++ b/test-automation/web-ui/tests/api/inventory/reservations.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-RES – Lagerreservierungen + * Quelle: GitHub Issues #4–#20 + */ +test.describe('TC-RES: Lagerreservierungen', () => { + async function setupStock( + request: Parameters[1]['request'], + token: string, + ): Promise<{ stockId: string }> { + const catRes = await request.post('/api/categories', { + data: { name: `RES-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + + const artRes = await request.post('/api/articles', { + data: { + name: `RES-Art-${Date.now()}`, + articleNumber: `RES-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 2.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: articleId } = await artRes.json(); + + const locRes = await request.post('/api/inventory/storage-locations', { + data: { name: `RES-Lager-${Date.now()}`, storageType: 'DRY_STORAGE' }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: storageLocationId } = await locRes.json(); + + const stockRes = await request.post('/api/inventory/stocks', { + data: { articleId, storageLocationId }, + headers: { Authorization: `Bearer ${token}` }, + }); + expect(stockRes.status()).toBe(201); + const { id: stockId } = await stockRes.json(); + + await request.post(`/api/inventory/stocks/${stockId}/batches`, { + data: { + batchId: `RES-B-${Date.now()}`, + batchType: 'PURCHASED', + quantityAmount: '100', + quantityUnit: 'KG', + expiryDate: '2027-06-30', + }, + headers: { Authorization: `Bearer ${token}` }, + }); + + return { stockId }; + } + + test('TC-RES-01: Reservierung anlegen', async ({ request, adminToken }) => { + const { stockId } = await setupStock(request, adminToken); + + const res = await request.post(`/api/inventory/stocks/${stockId}/reservations`, { + data: { + referenceType: 'PRODUCTION_ORDER', + referenceId: `PO-${Date.now()}`, + quantityAmount: '10', + quantityUnit: 'KG', + priority: 'NORMAL', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.id).toBeTruthy(); + }); + + test('TC-RES-02: Reservierung freigeben', async ({ request, adminToken }) => { + const { stockId } = await setupStock(request, adminToken); + + const createRes = await request.post(`/api/inventory/stocks/${stockId}/reservations`, { + data: { + referenceType: 'PRODUCTION_ORDER', + referenceId: `PO-REL-${Date.now()}`, + quantityAmount: '5', + quantityUnit: 'KG', + priority: 'NORMAL', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id: reservationId } = await createRes.json(); + + const releaseRes = await request.delete(`/api/inventory/stocks/${stockId}/reservations/${reservationId}`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(releaseRes.status()).toBe(204); + }); + + test('TC-RES-03: Reservierung bestätigen', async ({ request, adminToken }) => { + const { stockId } = await setupStock(request, adminToken); + + const createRes = await request.post(`/api/inventory/stocks/${stockId}/reservations`, { + data: { + referenceType: 'PRODUCTION_ORDER', + referenceId: `PO-CONF-${Date.now()}`, + quantityAmount: '8', + quantityUnit: 'KG', + priority: 'HIGH', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id: reservationId } = await createRes.json(); + + const confirmRes = await request.post(`/api/inventory/stocks/${stockId}/reservations/${reservationId}/confirm`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(confirmRes.status()).toBe(204); + }); +}); diff --git a/test-automation/web-ui/tests/api/inventory/stock.spec.ts b/test-automation/web-ui/tests/api/inventory/stock.spec.ts new file mode 100644 index 0000000..2c378b6 --- /dev/null +++ b/test-automation/web-ui/tests/api/inventory/stock.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-STOCK – Lagerbestand + * Quelle: GitHub Issues #4–#20 + */ +test.describe('TC-STOCK: Lagerbestand', () => { + async function createStorageLocation(request: Parameters[1]['request'], token: string): Promise { + const res = await request.post('/api/inventory/storage-locations', { + data: { name: `Lager-${Date.now()}`, storageType: 'DRY_STORAGE' }, + headers: { Authorization: `Bearer ${token}` }, + }); + const body = await res.json(); + return body.id; + } + + async function createArticle(request: Parameters[1]['request'], token: string): Promise { + const catRes = await request.post('/api/categories', { + data: { name: `ST-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + const artRes = await request.post('/api/articles', { + data: { + name: `ST-Art-${Date.now()}`, + articleNumber: `ST-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 1.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id } = await artRes.json(); + return id; + } + + test('TC-STOCK-01: Lagerort erstellen', async ({ request, adminToken }) => { + const res = await request.post('/api/inventory/storage-locations', { + data: { name: `Kühlraum-${Date.now()}`, storageType: 'COLD_ROOM', minTemperature: '2', maxTemperature: '8' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.storageType).toBe('COLD_ROOM'); + }); + + test('TC-STOCK-02: Lagerort ohne Name wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/inventory/storage-locations', { + data: { storageType: 'FREEZER' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-STOCK-03: Bestand anlegen', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + const articleId = await createArticle(request, adminToken); + + const res = await request.post('/api/inventory/stocks', { + data: { articleId, storageLocationId, minimumLevelAmount: '5', minimumLevelUnit: 'KG' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + }); + + test('TC-STOCK-04: Bestand ohne Artikel wird abgelehnt', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + const res = await request.post('/api/inventory/stocks', { + data: { storageLocationId }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-STOCK-05: Batch zum Bestand hinzufügen', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + const articleId = await createArticle(request, adminToken); + + const stockRes = await request.post('/api/inventory/stocks', { + data: { articleId, storageLocationId }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(stockRes.status()).toBe(201); + const { id: stockId } = await stockRes.json(); + + const batchRes = await request.post(`/api/inventory/stocks/${stockId}/batches`, { + data: { + batchId: `BATCH-${Date.now()}`, + batchType: 'PURCHASED', + quantityAmount: '10', + quantityUnit: 'KG', + expiryDate: '2026-12-31', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(batchRes.status()).toBe(201); + }); + + test('TC-STOCK-06: Bestände unterhalb Mindestmenge abfragen', async ({ request, adminToken }) => { + const res = await request.get('/api/inventory/stocks/below-minimum', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + expect(Array.isArray(await res.json())).toBe(true); + }); + + test('TC-STOCK-07: Bestände nach Lagerort filtern', async ({ request, adminToken }) => { + const storageLocationId = await createStorageLocation(request, adminToken); + const res = await request.get(`/api/inventory/stocks?storageLocationId=${storageLocationId}`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + expect(Array.isArray(await res.json())).toBe(true); + }); +}); diff --git a/test-automation/web-ui/tests/api/masterdata/articles.spec.ts b/test-automation/web-ui/tests/api/masterdata/articles.spec.ts new file mode 100644 index 0000000..eb51605 --- /dev/null +++ b/test-automation/web-ui/tests/api/masterdata/articles.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-ART – Artikel + * Quelle: GitHub Issue #64 + */ +test.describe('TC-ART: Artikel', () => { + async function createCategory(request: Parameters[1]['request'], token: string): Promise { + const res = await request.post('/api/categories', { + data: { name: `ART-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const body = await res.json(); + return body.id; + } + + test('TC-ART-01: Artikel erstellen – Pflichtfelder', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const res = await request.post('/api/articles', { + data: { + name: `Weizenbrot-${Date.now()}`, + articleNumber: `ART-${Date.now()}`, + categoryId, + unit: 'PIECE_FIXED', + priceModel: 'FIXED', + price: 2.99, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('ACTIVE'); + }); + + test('TC-ART-02: Artikel erscheint in Liste nach Erstellung', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const name = `ListArt-${Date.now()}`; + const articleNumber = `ART-LIST-${Date.now()}`; + await request.post('/api/articles', { + data: { name, articleNumber, categoryId, unit: 'KG', priceModel: 'FIXED', price: 5.0 }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + const res = await request.get('/api/articles', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + const list = Array.isArray(body) ? body : body.content ?? []; + expect(list.some((a: { name: string }) => a.name === name)).toBe(true); + }); + + test('TC-ART-03: Artikel ohne Name wird abgelehnt', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const res = await request.post('/api/articles', { + data: { articleNumber: `ART-${Date.now()}`, categoryId, unit: 'KG', priceModel: 'FIXED', price: 1.0 }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-ART-04: Artikel ohne Artikelnummer wird abgelehnt', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const res = await request.post('/api/articles', { + data: { name: `NoNumber-${Date.now()}`, categoryId, unit: 'KG', priceModel: 'FIXED', price: 1.0 }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-ART-05: Artikel deaktivieren', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const createRes = await request.post('/api/articles', { + data: { + name: `Deact-${Date.now()}`, + articleNumber: `ART-DEACT-${Date.now()}`, + categoryId, + unit: 'PIECE_FIXED', + priceModel: 'FIXED', + price: 1.5, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const deactRes = await request.post(`/api/articles/${id}/deactivate`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(deactRes.status()).toBe(200); + const body = await deactRes.json(); + expect(body.status).toBe('INACTIVE'); + }); + + test('TC-ART-06: Verkaufseinheit hinzufügen', async ({ request, adminToken }) => { + const categoryId = await createCategory(request, adminToken); + const createRes = await request.post('/api/articles', { + data: { + name: `SalesUnit-${Date.now()}`, + articleNumber: `ART-SU-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 3.0, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const suRes = await request.post(`/api/articles/${id}/sales-units`, { + data: { unit: 'HUNDRED_GRAM', priceModel: 'FIXED', price: 0.3 }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(suRes.status()).toBe(201); + }); +}); diff --git a/test-automation/web-ui/tests/api/masterdata/categories.spec.ts b/test-automation/web-ui/tests/api/masterdata/categories.spec.ts index d5acf93..d20de2b 100644 --- a/test-automation/web-ui/tests/api/masterdata/categories.spec.ts +++ b/test-automation/web-ui/tests/api/masterdata/categories.spec.ts @@ -6,7 +6,7 @@ import { test, expect } from '../../../fixtures/auth.fixture.js'; */ test.describe('TC-CAT: Produktkategorien', () => { test('TC-CAT-01: Kategorie erstellen – Happy Path', async ({ request, adminToken }) => { - const res = await request.post('/api/product-categories', { + const res = await request.post('/api/categories', { data: { name: 'Obst & Gemüse', description: 'Frische Produkte' }, headers: { Authorization: `Bearer ${adminToken}` }, }); @@ -17,7 +17,7 @@ test.describe('TC-CAT: Produktkategorien', () => { }); test('TC-CAT-02: Kategorie erstellen – ohne Beschreibung', async ({ request, adminToken }) => { - const res = await request.post('/api/product-categories', { + const res = await request.post('/api/categories', { data: { name: `Milchprodukte-${Date.now()}` }, headers: { Authorization: `Bearer ${adminToken}` }, }); @@ -26,11 +26,11 @@ test.describe('TC-CAT: Produktkategorien', () => { test('TC-CAT-04: Doppelter Name wird abgelehnt', async ({ request, adminToken }) => { const name = `Duplikat-${Date.now()}`; - await request.post('/api/product-categories', { + await request.post('/api/categories', { data: { name }, headers: { Authorization: `Bearer ${adminToken}` }, }); - const res = await request.post('/api/product-categories', { + const res = await request.post('/api/categories', { data: { name }, headers: { Authorization: `Bearer ${adminToken}` }, }); @@ -38,7 +38,7 @@ test.describe('TC-CAT: Produktkategorien', () => { }); test('TC-CAT-06: Leerer Name wird abgelehnt', async ({ request, adminToken }) => { - const res = await request.post('/api/product-categories', { + const res = await request.post('/api/categories', { data: { name: '' }, headers: { Authorization: `Bearer ${adminToken}` }, }); diff --git a/test-automation/web-ui/tests/api/masterdata/contracts.spec.ts b/test-automation/web-ui/tests/api/masterdata/contracts.spec.ts new file mode 100644 index 0000000..38a06c8 --- /dev/null +++ b/test-automation/web-ui/tests/api/masterdata/contracts.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-B2B – Rahmenverträge (Frame Contracts) + * Quelle: GitHub Issue #66 + */ +test.describe('TC-B2B: Rahmenverträge', () => { + const baseAddress = { + street: 'Handelsweg', + houseNumber: '10', + postalCode: '20095', + city: 'Hamburg', + country: 'DE', + phone: '+49 40 99999', + }; + + async function createB2bCustomer(request: Parameters[1]['request'], token: string): Promise { + const res = await request.post('/api/customers', { + data: { name: `B2B-${Date.now()}`, type: 'B2B', ...baseAddress, paymentDueDays: 30 }, + headers: { Authorization: `Bearer ${token}` }, + }); + const body = await res.json(); + return body.id; + } + + async function createArticleWithCategory(request: Parameters[1]['request'], token: string): Promise { + const catRes = await request.post('/api/categories', { + data: { name: `B2B-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + const artRes = await request.post('/api/articles', { + data: { + name: `B2B-Art-${Date.now()}`, + articleNumber: `B2B-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 4.5, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id } = await artRes.json(); + return id; + } + + test('TC-B2B-01: Rahmenvertrag setzen', async ({ request, adminToken }) => { + const customerId = await createB2bCustomer(request, adminToken); + const articleId = await createArticleWithCategory(request, adminToken); + + const res = await request.put(`/api/customers/${customerId}/frame-contract`, { + data: { + validFrom: '2026-01-01', + validUntil: '2026-12-31', + rhythm: 'WEEKLY', + lineItems: [{ articleId, agreedPrice: 4.0, agreedQuantity: 10.0, unit: 'KG' }], + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body.frameContract).toBeTruthy(); + }); + + test('TC-B2B-02: Rahmenvertrag ohne Positionen wird abgelehnt', async ({ request, adminToken }) => { + const customerId = await createB2bCustomer(request, adminToken); + + const res = await request.put(`/api/customers/${customerId}/frame-contract`, { + data: { rhythm: 'WEEKLY', lineItems: [] }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-B2B-03: Rahmenvertrag entfernen', async ({ request, adminToken }) => { + const customerId = await createB2bCustomer(request, adminToken); + const articleId = await createArticleWithCategory(request, adminToken); + + await request.put(`/api/customers/${customerId}/frame-contract`, { + data: { + rhythm: 'MONTHLY', + lineItems: [{ articleId, agreedPrice: 3.0 }], + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + const res = await request.delete(`/api/customers/${customerId}/frame-contract`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(204); + }); +}); diff --git a/test-automation/web-ui/tests/api/masterdata/customers.spec.ts b/test-automation/web-ui/tests/api/masterdata/customers.spec.ts new file mode 100644 index 0000000..617de47 --- /dev/null +++ b/test-automation/web-ui/tests/api/masterdata/customers.spec.ts @@ -0,0 +1,119 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-CUS – Kunden + * Quelle: GitHub Issue #65 + */ +test.describe('TC-CUS: Kunden', () => { + const baseAddress = { + street: 'Musterstraße', + houseNumber: '1', + postalCode: '10115', + city: 'Berlin', + country: 'DE', + phone: '+49 30 12345', + }; + + test('TC-CUS-01: Kunde erstellen – Pflichtfelder (B2C)', async ({ request, adminToken }) => { + const res = await request.post('/api/customers', { + data: { name: `Müller ${Date.now()}`, type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('ACTIVE'); + expect(body.type).toBe('B2C'); + }); + + test('TC-CUS-02: B2B-Kunde erstellen', async ({ request, adminToken }) => { + const res = await request.post('/api/customers', { + data: { + name: `B2B GmbH ${Date.now()}`, + type: 'B2B', + ...baseAddress, + contactPerson: 'Max Mustermann', + paymentDueDays: 30, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.type).toBe('B2B'); + }); + + test('TC-CUS-03: Kunde erscheint in Liste nach Erstellung', async ({ request, adminToken }) => { + const name = `ListKunde-${Date.now()}`; + await request.post('/api/customers', { + data: { name, type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + const res = await request.get('/api/customers', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + const list = Array.isArray(body) ? body : body.content ?? []; + expect(list.some((c: { name: string }) => c.name === name)).toBe(true); + }); + + test('TC-CUS-04: Kunde ohne Name wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/customers', { + data: { type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-CUS-05: Lieferadresse hinzufügen', async ({ request, adminToken }) => { + const createRes = await request.post('/api/customers', { + data: { name: `DelivAddr-${Date.now()}`, type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const addrRes = await request.post(`/api/customers/${id}/delivery-addresses`, { + data: { + label: 'Lager', + street: 'Lagerstraße', + houseNumber: '5', + postalCode: '10117', + city: 'Berlin', + country: 'DE', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(addrRes.status()).toBe(201); + }); + + test('TC-CUS-06: Kunde deaktivieren', async ({ request, adminToken }) => { + const createRes = await request.post('/api/customers', { + data: { name: `Deact-${Date.now()}`, type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const deactRes = await request.post(`/api/customers/${id}/deactivate`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(deactRes.status()).toBe(200); + const body = await deactRes.json(); + expect(body.status).toBe('INACTIVE'); + }); + + test('TC-CUS-07: Präferenzen setzen', async ({ request, adminToken }) => { + const createRes = await request.post('/api/customers', { + data: { name: `Prefs-${Date.now()}`, type: 'B2C', ...baseAddress }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const prefsRes = await request.put(`/api/customers/${id}/preferences`, { + data: { preferences: ['BIO', 'REGIONAL'] }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(prefsRes.status()).toBe(200); + }); +}); diff --git a/test-automation/web-ui/tests/api/production/batches.spec.ts b/test-automation/web-ui/tests/api/production/batches.spec.ts new file mode 100644 index 0000000..a8d0324 --- /dev/null +++ b/test-automation/web-ui/tests/api/production/batches.spec.ts @@ -0,0 +1,151 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-BAT – Produktionschargen + * Quelle: GitHub Issues #33–#36 + */ +test.describe('TC-BAT: Produktionschargen', () => { + async function createActiveRecipe(request: Parameters[1]['request'], token: string): Promise { + const catRes = await request.post('/api/categories', { + data: { name: `BAT-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + const artRes = await request.post('/api/articles', { + data: { + name: `BAT-Art-${Date.now()}`, + articleNumber: `BAT-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 1.5, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: articleId } = await artRes.json(); + + const recipeRes = await request.post('/api/recipes', { + data: { + name: `BAT-Rezept-${Date.now()}`, + version: 1, + type: 'FINISHED_PRODUCT', + outputQuantity: '10', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: recipeId } = await recipeRes.json(); + + await request.post(`/api/recipes/${recipeId}/activate`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return recipeId; + } + + test('TC-BAT-01: Charge planen – PLANNED Status', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const res = await request.post('/api/production/batches', { + data: { + recipeId, + plannedQuantity: '20', + plannedQuantityUnit: 'KG', + productionDate: '2026-04-01', + bestBeforeDate: '2026-04-08', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('PLANNED'); + expect(body.batchNumber).toBeTruthy(); + }); + + test('TC-BAT-02: Charge starten', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const planRes = await request.post('/api/production/batches', { + data: { + recipeId, + plannedQuantity: '5', + plannedQuantityUnit: 'KG', + productionDate: '2026-04-02', + bestBeforeDate: '2026-04-09', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(planRes.status()).toBe(201); + const { id } = await planRes.json(); + + const startRes = await request.post(`/api/production/batches/${id}/start`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(startRes.status()).toBe(200); + const body = await startRes.json(); + expect(body.status).toBe('IN_PRODUCTION'); + }); + + test('TC-BAT-03: Charge abschließen', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const planRes = await request.post('/api/production/batches', { + data: { + recipeId, + plannedQuantity: '8', + plannedQuantityUnit: 'KG', + productionDate: '2026-04-03', + bestBeforeDate: '2026-04-10', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(planRes.status()).toBe(201); + const { id } = await planRes.json(); + + await request.post(`/api/production/batches/${id}/start`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + const completeRes = await request.post(`/api/production/batches/${id}/complete`, { + data: { actualQuantity: '7.5', actualQuantityUnit: 'KG', waste: '0.5', wasteUnit: 'KG' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(completeRes.status()).toBe(200); + const body = await completeRes.json(); + expect(body.status).toBe('COMPLETED'); + }); + + test('TC-BAT-04: Charge ohne Rezept wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/production/batches', { + data: { + plannedQuantity: '10', + plannedQuantityUnit: 'KG', + productionDate: '2026-04-01', + bestBeforeDate: '2026-04-08', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-BAT-05: Charge nach Nummer suchen', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const planRes = await request.post('/api/production/batches', { + data: { + recipeId, + plannedQuantity: '3', + plannedQuantityUnit: 'KG', + productionDate: '2026-04-04', + bestBeforeDate: '2026-04-11', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(planRes.status()).toBe(201); + const { batchNumber } = await planRes.json(); + + const findRes = await request.get(`/api/production/batches/by-number/${batchNumber}`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(findRes.status()).toBe(200); + const body = await findRes.json(); + expect(body.batchNumber).toBe(batchNumber); + }); +}); diff --git a/test-automation/web-ui/tests/api/production/orders.spec.ts b/test-automation/web-ui/tests/api/production/orders.spec.ts new file mode 100644 index 0000000..3c8ba13 --- /dev/null +++ b/test-automation/web-ui/tests/api/production/orders.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-ORD – Produktionsaufträge + * Quelle: GitHub Issues #38–#42 + */ +test.describe('TC-ORD: Produktionsaufträge', () => { + async function createActiveRecipe(request: Parameters[1]['request'], token: string): Promise { + const catRes = await request.post('/api/categories', { + data: { name: `ORD-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + const artRes = await request.post('/api/articles', { + data: { + name: `ORD-Art-${Date.now()}`, + articleNumber: `ORD-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 1.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: articleId } = await artRes.json(); + + const recipeRes = await request.post('/api/recipes', { + data: { + name: `ORD-Rezept-${Date.now()}`, + version: 1, + type: 'FINISHED_PRODUCT', + outputQuantity: '10', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: recipeId } = await recipeRes.json(); + + await request.post(`/api/recipes/${recipeId}/activate`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return recipeId; + } + + test('TC-ORD-01: Produktionsauftrag erstellen – PLANNED Status', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const res = await request.post('/api/production/production-orders', { + data: { + recipeId, + plannedQuantity: '50', + plannedQuantityUnit: 'KG', + plannedDate: '2026-05-01', + priority: 'NORMAL', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('PLANNED'); + }); + + test('TC-ORD-02: Auftrag ohne Rezept wird abgelehnt', async ({ request, adminToken }) => { + const res = await request.post('/api/production/production-orders', { + data: { + plannedQuantity: '10', + plannedQuantityUnit: 'KG', + plannedDate: '2026-05-01', + priority: 'NORMAL', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-ORD-03: Auftrag freigeben', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const createRes = await request.post('/api/production/production-orders', { + data: { + recipeId, + plannedQuantity: '30', + plannedQuantityUnit: 'KG', + plannedDate: '2026-05-02', + priority: 'HIGH', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const releaseRes = await request.post(`/api/production/production-orders/${id}/release`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(releaseRes.status()).toBe(200); + const body = await releaseRes.json(); + expect(body.status).toBe('RELEASED'); + }); + + test('TC-ORD-04: Auftrag stornieren', async ({ request, adminToken }) => { + const recipeId = await createActiveRecipe(request, adminToken); + const createRes = await request.post('/api/production/production-orders', { + data: { + recipeId, + plannedQuantity: '15', + plannedQuantityUnit: 'KG', + plannedDate: '2026-05-03', + priority: 'LOW', + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(createRes.status()).toBe(201); + const { id } = await createRes.json(); + + const cancelRes = await request.post(`/api/production/production-orders/${id}/cancel`, { + data: { reason: 'Test-Stornierung' }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(cancelRes.status()).toBe(200); + const body = await cancelRes.json(); + expect(body.status).toBe('CANCELLED'); + }); + + test('TC-ORD-05: Aufträge nach Status filtern', async ({ request, adminToken }) => { + const res = await request.get('/api/production/production-orders?status=PLANNED', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(Array.isArray(body)).toBe(true); + }); +}); diff --git a/test-automation/web-ui/tests/api/production/recipes.spec.ts b/test-automation/web-ui/tests/api/production/recipes.spec.ts new file mode 100644 index 0000000..7f226b3 --- /dev/null +++ b/test-automation/web-ui/tests/api/production/recipes.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-REC – Rezepte + * Quelle: GitHub Issues #26–#32 + */ +test.describe('TC-REC: Rezepte', () => { + async function createArticle(request: Parameters[1]['request'], token: string): Promise { + const catRes = await request.post('/api/categories', { + data: { name: `Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + const artRes = await request.post('/api/articles', { + data: { + name: `Art-${Date.now()}`, + articleNumber: `ART-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 2.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id } = await artRes.json(); + return id; + } + + test('TC-REC-01: Rezept erstellen – DRAFT Status', async ({ request, adminToken }) => { + const articleId = await createArticle(request, adminToken); + const res = await request.post('/api/recipes', { + data: { + name: `Brot-${Date.now()}`, + version: 1, + type: 'FINISHED_PRODUCT', + outputQuantity: '10', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(201); + const body = await res.json(); + expect(body.status).toBe('DRAFT'); + }); + + test('TC-REC-02: Rezept ohne Name wird abgelehnt', async ({ request, adminToken }) => { + const articleId = await createArticle(request, adminToken); + const res = await request.post('/api/recipes', { + data: { version: 1, type: 'FINISHED_PRODUCT', outputQuantity: '10', outputUom: 'KG', articleId }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(400); + }); + + test('TC-REC-03: Zutat zu Rezept hinzufügen', async ({ request, adminToken }) => { + const articleId = await createArticle(request, adminToken); + const recipeRes = await request.post('/api/recipes', { + data: { + name: `Rezept-Zutaten-${Date.now()}`, + version: 1, + type: 'FINISHED_PRODUCT', + outputQuantity: '5', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(recipeRes.status()).toBe(201); + const { id: recipeId } = await recipeRes.json(); + + const ingredientArticleId = await createArticle(request, adminToken); + const ingRes = await request.post(`/api/recipes/${recipeId}/ingredients`, { + data: { position: 1, articleId: ingredientArticleId, quantity: '2', uom: 'KG', substitutable: false }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(ingRes.status()).toBe(201); + }); + + test('TC-REC-04: Rezept aktivieren', async ({ request, adminToken }) => { + const articleId = await createArticle(request, adminToken); + const recipeRes = await request.post('/api/recipes', { + data: { + name: `Aktivieren-${Date.now()}`, + version: 1, + type: 'INTERMEDIATE', + outputQuantity: '3', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(recipeRes.status()).toBe(201); + const { id: recipeId } = await recipeRes.json(); + + const activateRes = await request.post(`/api/recipes/${recipeId}/activate`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(activateRes.status()).toBe(200); + const body = await activateRes.json(); + expect(body.status).toBe('ACTIVE'); + }); + + test('TC-REC-05: Rezept archivieren', async ({ request, adminToken }) => { + const articleId = await createArticle(request, adminToken); + const recipeRes = await request.post('/api/recipes', { + data: { + name: `Archiv-${Date.now()}`, + version: 1, + type: 'RAW_MATERIAL', + outputQuantity: '1', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(recipeRes.status()).toBe(201); + const { id: recipeId } = await recipeRes.json(); + + await request.post(`/api/recipes/${recipeId}/activate`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + + const archiveRes = await request.post(`/api/recipes/${recipeId}/archive`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(archiveRes.status()).toBe(200); + const body = await archiveRes.json(); + expect(body.status).toBe('ARCHIVED'); + }); + + test('TC-REC-06: Rezepte nach Status filtern', async ({ request, adminToken }) => { + const res = await request.get('/api/recipes?status=DRAFT', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + const list = Array.isArray(body) ? body : body.content ?? []; + expect(Array.isArray(list)).toBe(true); + }); +}); diff --git a/test-automation/web-ui/tests/api/production/traceability.spec.ts b/test-automation/web-ui/tests/api/production/traceability.spec.ts new file mode 100644 index 0000000..c9df3c0 --- /dev/null +++ b/test-automation/web-ui/tests/api/production/traceability.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '../../../fixtures/auth.fixture.js'; + +/** + * TC-TRACE – Chargen-Rückverfolgung + * Quelle: GitHub Issues #43–#44 + */ +test.describe('TC-TRACE: Chargen-Rückverfolgung', () => { + async function createPlannedBatch(request: Parameters[1]['request'], token: string): Promise<{ batchId: string; batchNumber: string }> { + const catRes = await request.post('/api/categories', { + data: { name: `TRC-Kat-${Date.now()}` }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: categoryId } = await catRes.json(); + + const artRes = await request.post('/api/articles', { + data: { + name: `TRC-Art-${Date.now()}`, + articleNumber: `TRC-${Date.now()}`, + categoryId, + unit: 'KG', + priceModel: 'FIXED', + price: 2.0, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: articleId } = await artRes.json(); + + const recipeRes = await request.post('/api/recipes', { + data: { + name: `TRC-Rezept-${Date.now()}`, + version: 1, + type: 'FINISHED_PRODUCT', + outputQuantity: '5', + outputUom: 'KG', + articleId, + }, + headers: { Authorization: `Bearer ${token}` }, + }); + const { id: recipeId } = await recipeRes.json(); + + await request.post(`/api/recipes/${recipeId}/activate`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + const batchRes = await request.post('/api/production/batches', { + data: { + recipeId, + plannedQuantity: '5', + plannedQuantityUnit: 'KG', + productionDate: '2026-06-01', + bestBeforeDate: '2026-06-08', + }, + headers: { Authorization: `Bearer ${token}` }, + }); + expect(batchRes.status()).toBe(201); + const batch = await batchRes.json(); + return { batchId: batch.id, batchNumber: batch.batchNumber }; + } + + test('TC-TRACE-01: Vorwärts-Verfolgung einer Charge', async ({ request, adminToken }) => { + const { batchId } = await createPlannedBatch(request, adminToken); + + const res = await request.get(`/api/production/batches/${batchId}/trace-forward`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body).toHaveProperty('batchId'); + }); + + test('TC-TRACE-02: Rückwärts-Verfolgung einer Charge', async ({ request, adminToken }) => { + const { batchId } = await createPlannedBatch(request, adminToken); + + const res = await request.get(`/api/production/batches/${batchId}/trace-backward`, { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect(res.status()).toBe(200); + const body = await res.json(); + expect(body).toHaveProperty('batchId'); + }); + + test('TC-TRACE-03: Rückverfolgung einer nicht vorhandenen Charge gibt 404', async ({ request, adminToken }) => { + const res = await request.get('/api/production/batches/non-existent-id/trace-forward', { + headers: { Authorization: `Bearer ${adminToken}` }, + }); + expect([404, 400]).toContain(res.status()); + }); +});