From 8a982ee8043882e8eea8ebb635354121627a0447 Mon Sep 17 00:00:00 2001 From: Chukwuemeka Date: Thu, 24 Sep 2026 16:50:39 +0200 Subject: [PATCH 1/3] fix(resource-deleter): await integration teardown and self-heal setup `afterAll` passed a callback to `Promise.each` that called `clearData` without returning it, so nothing was awaited. Teardown resolved while the deletes were still in flight and Jest tore the process down mid-request, leaving fixtures behind in the shared test project. Once leaked, the residue was permanent: `beforeAll` seeded without clearing first, so every later run failed with BadRequest: A duplicate value '"fooCatKey"' exists for field 'key' and that cascaded to every test in the suite, including ones that never touch the API. Return the promise so teardown actually completes, and clear before seeding so the suite recovers on its own from an interrupted run -- the same pattern personal-data-erasure already uses. The setup timeout goes to 60s to cover the added clear step. Co-Authored-By: Claude Opus 5 (1M context) --- packages/resource-deleter/test/integration/cli.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/resource-deleter/test/integration/cli.spec.ts b/packages/resource-deleter/test/integration/cli.spec.ts index cf2f899..e85f9d6 100644 --- a/packages/resource-deleter/test/integration/cli.spec.ts +++ b/packages/resource-deleter/test/integration/cli.spec.ts @@ -59,16 +59,22 @@ describe('Resource Deleter', () => { credentials, }; + // clear anything a previous run left behind before seeding, so the + // suite recovers on its own from an interrupted teardown + await Promise.each(Object.keys(resources), (name: MethodNames) => { + return clearData(apiConfig, name); + }); + // create resources on API await Promise.each(Object.keys(resources), (name: MethodNames) => { return createData(apiConfig, name, resources[name]); }); - }, 30000); + }, 60000); // clear resources on API afterAll(async () => { await Promise.each(Object.keys(resources), (name: MethodNames) => { - clearData(apiConfig, name); + return clearData(apiConfig, name); }); }, 45000); From d91d498dc6fa4746e88a89d6deed58ca5e28d205 Mon Sep 17 00:00:00 2001 From: Chukwuemeka Date: Thu, 24 Sep 2026 16:54:13 +0200 Subject: [PATCH 2/3] fix(resource-deleter): clear fixtures in reverse dependency order Awaiting the teardown exposed a second defect it had been hiding: the clear loop walks resources in declaration order, but commercetools refuses to delete a resource that is still referenced, so removing product-types before products fails with Can not delete a product-type while it is referenced by at least one product Previously those rejections were discarded along with the un-returned promise, so the ordering was never exercised. Extract a single `clearAllResources` helper that walks the keys in reverse, removing dependents first -- the order the per-resource delete tests already assume via `Object.keys(resources).reverse()`. Teardown timeout matches setup at 60s. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/integration/cli.spec.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/resource-deleter/test/integration/cli.spec.ts b/packages/resource-deleter/test/integration/cli.spec.ts index e85f9d6..af1cad1 100644 --- a/packages/resource-deleter/test/integration/cli.spec.ts +++ b/packages/resource-deleter/test/integration/cli.spec.ts @@ -49,6 +49,15 @@ describe('Resource Deleter', () => { return client.execute(request); } + // Deleting in declaration order fails, because a resource cannot be removed + // while another still references it (a product-type referenced by a product, + // for example). Reverse order removes dependents first -- the same order the + // per-resource delete tests below rely on. + const clearAllResources = () => + Promise.each(Object.keys(resources).reverse(), (name: MethodNames) => { + return clearData(apiConfig, name); + }); + beforeAll(async () => { // Get test credentials const credentials = await getCredentials(); @@ -61,9 +70,7 @@ describe('Resource Deleter', () => { // clear anything a previous run left behind before seeding, so the // suite recovers on its own from an interrupted teardown - await Promise.each(Object.keys(resources), (name: MethodNames) => { - return clearData(apiConfig, name); - }); + await clearAllResources(); // create resources on API await Promise.each(Object.keys(resources), (name: MethodNames) => { @@ -73,10 +80,8 @@ describe('Resource Deleter', () => { // clear resources on API afterAll(async () => { - await Promise.each(Object.keys(resources), (name: MethodNames) => { - return clearData(apiConfig, name); - }); - }, 45000); + await clearAllResources(); + }, 60000); describe('CLI basic functionality', () => { it('should print usage information given the help flag', async () => { From d752dbd8d7c90ce858ca4fcb6df06a8091b9fa29 Mon Sep 17 00:00:00 2001 From: Chukwuemeka Date: Thu, 24 Sep 2026 16:58:01 +0200 Subject: [PATCH 3/3] fix(resource-deleter): unpublish products before deleting in clearData With teardown ordering fixed, the remaining failure was BadRequest: Product cannot be deleted as long as it is published in a catalog `clearData` deleted by id without unpublishing, so any published product survived cleanup and poisoned later runs. The deleter itself already handles this (src/main.ts:117), but the test helper did not. Unpublish first when `masterData.published` is set, then delete with the bumped version. The unpublish overrides a built request rather than calling `.post()`, because the update-action union across every resource builder is too complex for TypeScript to represent -- the same workaround src/main.ts uses. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/integration/helpers/helpers.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/resource-deleter/test/integration/helpers/helpers.ts b/packages/resource-deleter/test/integration/helpers/helpers.ts index 107fd49..8704775 100644 --- a/packages/resource-deleter/test/integration/helpers/helpers.ts +++ b/packages/resource-deleter/test/integration/helpers/helpers.ts @@ -41,7 +41,7 @@ export function clearData( : payload.body.results; return Promise.all( - results.map((result): Promise> => { + results.map(async (result): Promise> => { let request: ClientRequest; if (service instanceof ByProjectKeyCustomObjectsRequestBuilder) { @@ -53,9 +53,28 @@ export function clearData( .delete({ queryArgs: { version: result.version } }) .clientRequest(); } else { - request = service - .withId({ ID: result.id }) - .delete({ queryArgs: { version: result.version } }) + const serviceBuilder = service.withId({ ID: result.id }); + let version = result.version; + + // A published product cannot be deleted, so unpublish it first. + // Unpublishing bumps the version, which the delete then needs. + // Mirrors how the deleter itself handles this in src/main.ts, + // including overriding a built request rather than calling + // .post(), whose update-action union is too complex to type. + if (result.masterData?.published) { + await client.execute({ + ...serviceBuilder.get().clientRequest(), + method: 'POST', + body: JSON.stringify({ + version, + actions: [{ action: 'unpublish' }], + }), + }); + version += 1; + } + + request = serviceBuilder + .delete({ queryArgs: { version } }) .clientRequest(); }