From 7308885094a6079fb8755c59afe643d79bc0691b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 9 Apr 2026 10:28:42 +0200 Subject: [PATCH] Fix MySQL existence check and add router/store tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MySQL UPDATE returns 0 affected rows when values are unchanged, so detect existence via SELECT within the transaction instead. - Add integration tests for PUT /locations/:id in createRouter.test.ts. - Add changeset for catalog-react catalogApiMock update. Signed-off-by: Fredrik Adelöw Made-with: Cursor --- .../location-entity-ref-catalog-react.md | 5 +++ .../src/providers/DefaultLocationStore.ts | 17 +++++---- .../src/service/createRouter.test.ts | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 .changeset/location-entity-ref-catalog-react.md diff --git a/.changeset/location-entity-ref-catalog-react.md b/.changeset/location-entity-ref-catalog-react.md new file mode 100644 index 0000000000..780922dcf2 --- /dev/null +++ b/.changeset/location-entity-ref-catalog-react.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Updated `catalogApiMock` to include the new `updateLocation` method stub, keeping it in sync with the `CatalogApi` interface. diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts index 808d3cf9a7..1bf744f1c9 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts @@ -210,16 +210,19 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { throw new Error('location store is not initialized'); } - // MySQL doesn't support UPDATE ... RETURNING, so we fall back to checking - // the affected row count and then doing a separate SELECT. + // MySQL doesn't support UPDATE ... RETURNING. MySQL also reports 0 affected + // rows when the new values are identical to the old ones, so we can't rely + // on the row count to detect existence. Instead we SELECT to check existence + // first and then UPDATE inside a transaction. let row: DbLocationsRow | undefined; if (this.db.client.config.client.includes('mysql')) { await this.db.transaction(async tx => { - const count = await tx('locations') - .where({ id }) - .update({ type: location.type, target: location.target }); - if (Number(count) > 0) { - [row] = await tx('locations').where({ id }).select(); + [row] = await tx('locations').where({ id }).select(); + if (row) { + await tx('locations') + .where({ id }) + .update({ type: location.type, target: location.target }); + row = { ...row, type: location.type, target: location.target }; } }); } else { diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index c196161a40..7092572ae6 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -1175,6 +1175,43 @@ describe('createRouter readonly disabled', () => { }); }); + describe('PUT /locations/:id', () => { + it('rejects malformed body', async () => { + const response = await request(app) + .put('/locations/foo') + .send({ typez: 'url', target: 'https://example.com' }); + + expect(locationService.updateLocation).not.toHaveBeenCalled(); + expect(response.status).toEqual(400); + }); + + it('updates the location and returns it', async () => { + const spec: LocationInput = { + type: 'url', + target: 'https://example.com/new', + }; + + locationService.updateLocation.mockResolvedValue({ + id: 'foo', + ...spec, + entityRef: 'location:default/generated-foo', + }); + + const response = await request(app).put('/locations/foo').send(spec); + + expect(locationService.updateLocation).toHaveBeenCalledTimes(1); + expect(locationService.updateLocation).toHaveBeenCalledWith('foo', spec, { + credentials: mockCredentials.user(), + }); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + id: 'foo', + ...spec, + entityRef: 'location:default/generated-foo', + }); + }); + }); + describe('GET /locations/by-entity/:kind/:namespace/:name', () => { it('happy path: gets location by entity ref', async () => { const location: Location = {