Fix MySQL existence check and add router/store tests

- 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 <freben@spotify.com>
Made-with: Cursor
This commit is contained in:
Fredrik Adelöw
2026-04-09 10:28:42 +02:00
parent a102296f7f
commit 7308885094
3 changed files with 52 additions and 7 deletions
@@ -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.
@@ -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<DbLocationsRow>('locations')
.where({ id })
.update({ type: location.type, target: location.target });
if (Number(count) > 0) {
[row] = await tx<DbLocationsRow>('locations').where({ id }).select();
[row] = await tx<DbLocationsRow>('locations').where({ id }).select();
if (row) {
await tx<DbLocationsRow>('locations')
.where({ id })
.update({ type: location.type, target: location.target });
row = { ...row, type: location.type, target: location.target };
}
});
} else {
@@ -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 = {