frontend-app-api: address remaining review feedback

Move the deprecated createSpecializedApp API holder option back under advanced and disable DataLoader caching in IdentityPermissionApi so batching stays limited to same-tick requests.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-16 19:21:36 +01:00
parent d7ed46b83c
commit c6d96e7426
2 changed files with 100 additions and 0 deletions
@@ -0,0 +1,97 @@
/*
* Copyright 2026 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { mockApis } from '@backstage/test-utils';
import {
createPermission,
PermissionClient,
} from '@backstage/plugin-permission-common';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { IdentityPermissionApi } from './IdentityPermissionApi';
describe('IdentityPermissionApi', () => {
const permission = createPermission({
name: 'test.permission',
attributes: {},
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should batch requests that arrive on the same tick', async () => {
const authorizeSpy = jest
.spyOn(PermissionClient.prototype, 'authorize')
.mockResolvedValue([
{ result: AuthorizeResult.ALLOW },
{ result: AuthorizeResult.DENY },
]);
const api = IdentityPermissionApi.create({
config: new ConfigReader({}),
discovery: mockApis.discovery(),
identity: mockApis.identity(),
});
const firstRequest = { permission };
const secondRequest = { permission };
const [firstResponse, secondResponse] = await Promise.all([
api.authorize(firstRequest),
api.authorize(secondRequest),
]);
expect(firstResponse.result).toBe(AuthorizeResult.ALLOW);
expect(secondResponse.result).toBe(AuthorizeResult.DENY);
expect(authorizeSpy).toHaveBeenCalledTimes(1);
expect(authorizeSpy).toHaveBeenCalledWith(
[firstRequest, secondRequest],
expect.anything(),
);
});
it('should not cache requests across ticks', async () => {
const authorizeSpy = jest
.spyOn(PermissionClient.prototype, 'authorize')
.mockResolvedValue([{ result: AuthorizeResult.ALLOW }]);
const identityApi = mockApis.identity();
const credentialsSpy = jest
.spyOn(identityApi, 'getCredentials')
.mockResolvedValueOnce({ token: 'first-token' })
.mockResolvedValueOnce({ token: 'second-token' });
const api = IdentityPermissionApi.create({
config: new ConfigReader({}),
discovery: mockApis.discovery(),
identity: identityApi,
});
const request = { permission };
await api.authorize(request);
await api.authorize(request);
expect(authorizeSpy).toHaveBeenCalledTimes(2);
expect(credentialsSpy).toHaveBeenCalledTimes(2);
expect(authorizeSpy).toHaveBeenNthCalledWith(
1,
[request],
expect.objectContaining({ token: 'first-token' }),
);
expect(authorizeSpy).toHaveBeenNthCalledWith(
2,
[request],
expect.objectContaining({ token: 'second-token' }),
);
});
});
@@ -45,6 +45,9 @@ export class IdentityPermissionApi implements PermissionApi {
const credentials = await identityApi.getCredentials();
return permissionClient.authorize([...requests], credentials);
},
{
cache: false,
},
);
}