Skip to content

Commit 6d37ff4

Browse files
authored
feat: OI-533 FE add pairwise in admin control panel (#916)
* feat: add validate and plan endpoint * feat: add new client and header to update/create function * feat: add new zod schema * feat: add pairwise switch * test: register ut * refactor: code style * test: ut * refactor: comments implementation * test: uncommitted test * chore: add changeset
1 parent a98d33c commit 6d37ff4

File tree

16 files changed

+639
-42
lines changed

16 files changed

+639
-42
lines changed

.changeset/cyan-actors-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"oi-control-panel": minor
3+
---
4+
5+
Add switch and supporting logic to enable the Pairwise feature

.changeset/forty-cars-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"oneid-lambda-client-registration": minor
3+
---
4+
5+
Change headers for pairwise feature

src/infra/api/oi-admin.tpl.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"operationId": "Post_oidc_register",
3636
"parameters":[
3737
{
38-
"name": "PDV-X-Api-Key",
38+
"name": "Plan-Api-Key",
3939
"in": "header",
4040
"description": "PDV API key used for plan validation when pairwise = true",
4141
"required": false,
@@ -44,7 +44,7 @@
4444
}
4545
},
4646
{
47-
"name": "PDV-Plan-Name",
47+
"name": "Plan-Name",
4848
"in": "header",
4949
"description": "PDV plan name associated with the PDV API key when pairwise = true",
5050
"required": false,
@@ -168,7 +168,7 @@
168168
}
169169
},
170170
{
171-
"name": "PDV-X-Api-Key",
171+
"name": "Plan-Api-Key",
172172
"in": "header",
173173
"description": "PDV API key used for plan validation when pairwise = true",
174174
"required": false,
@@ -177,7 +177,7 @@
177177
}
178178
},
179179
{
180-
"name": "PDV-Plan-Name",
180+
"name": "Plan-Name",
181181
"in": "header",
182182
"description": "PDV plan name associated with the PDV API key when pairwise = true",
183183
"required": false,

src/oneid/oneid-control-panel/.env.dev

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ VITE_PUBLIC_URL=https://admin.dev.oneid.pagopa.it
77

88
VITE_URL_CDN=/assets
99
VITE_URL_API_LOGIN=/oidc/register
10+
VITE_URL_API_PLAN_LIST_SCHEMA=/plan-list
11+
VITE_URL_API_VALIDATE_PLAN_SCHEMA=/validate-api-key
1012
VITE_URL_API_REGISTER=https://admin.dev.oneid.pagopa.it/oidc/register
1113
VITE_URL_API_CLIENT_USERS=https://admin.dev.oneid.pagopa.it/client-manager/client-users
1214

src/oneid/oneid-control-panel/.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ VITE_PUBLIC_URL=http://localhost:5173
77

88
VITE_URL_CDN=/assets
99
VITE_URL_API_LOGIN=/oidc/register
10+
VITE_URL_API_PLAN_LIST_SCHEMA=/plan-list
11+
VITE_URL_API_VALIDATE_PLAN_SCHEMA=/validate-api-key
1012
VITE_URL_API_REGISTER=https://admin.dev.oneid.pagopa.it/oidc/register
1113
VITE_URL_API_CLIENT_USERS=https://admin.dev.oneid.pagopa.it/client-manager/client-users
1214

src/oneid/oneid-control-panel/.env.prod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ VITE_PUBLIC_URL=https://admin.oneid.pagopa.it
77

88
VITE_URL_CDN=/assets
99
VITE_URL_API_LOGIN=/oidc/register
10+
VITE_URL_API_PLAN_LIST_SCHEMA=/plan-list
11+
VITE_URL_API_VALIDATE_PLAN_SCHEMA=/validate-api-key
1012
VITE_URL_API_REGISTER=https://admin.oneid.pagopa.it/oidc/register
1113

1214
# Cognito user pool

src/oneid/oneid-control-panel/.env.uat

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ VITE_PUBLIC_URL=https://admin.uat.oneid.pagopa.it
77

88
VITE_URL_CDN=/assets
99
VITE_URL_API_LOGIN=/oidc/register
10+
VITE_URL_API_PLAN_LIST_SCHEMA=/plan-list
11+
VITE_URL_API_VALIDATE_PLAN_SCHEMA=/validate-api-key
1012
VITE_URL_API_REGISTER=https://admin.uat.oneid.pagopa.it/oidc/register
1113
VITE_URL_API_CLIENT_USERS=https://admin.uat.oneid.pagopa.it/client-manager/client-users
1214

src/oneid/oneid-control-panel/src/api/register.test.ts

Lines changed: 180 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
2-
import { getClientData, createOrUpdateClient } from './register';
2+
import {
3+
getClientData,
4+
createOrUpdateClient,
5+
getPlanList,
6+
validateApiKeyPlan,
7+
} from './register';
38
import { ENV } from '../utils/env';
4-
import { Client, SamlAttribute, SpidLevel } from '../types/api';
9+
import {
10+
Client,
11+
SamlAttribute,
12+
SpidLevel,
13+
ValidateApiKeySchema,
14+
ValidatePlanSchema,
15+
} from '../types/api';
516

617
vi.mock('../utils/env', () => ({
718
ENV: { URL_API: { REGISTER: 'https://api.example.com/register' } },
819
}));
920

21+
const unexpectedError = 'Unexpected error';
22+
1023
const axiosMock = vi.hoisted(() => ({
1124
get: vi.fn(),
1225
post: vi.fn(),
@@ -53,7 +66,7 @@ describe('getClientData', () => {
5366
vi.restoreAllMocks();
5467
});
5568

56-
it('fetches client data successfully', async () => {
69+
it('fetches plan data successfully', async () => {
5770
axiosMock.get.mockResolvedValueOnce({
5871
data: mockClientData,
5972
});
@@ -151,6 +164,43 @@ describe('createOrUpdateClient', () => {
151164
);
152165
});
153166

167+
it('updates an existing client successfully pairwise enabled', async () => {
168+
const clientId = 'existing-client-id';
169+
axiosMock.put.mockResolvedValueOnce({
170+
data: {
171+
...mockClientData,
172+
pairwise: true,
173+
},
174+
});
175+
176+
const mockValidatePlanSchema: ValidatePlanSchema = {
177+
apiKeyId: 'plan1',
178+
apiKeyValue: 'keyValue',
179+
};
180+
181+
const result = await createOrUpdateClient(
182+
mockClientData,
183+
token,
184+
clientId,
185+
mockValidatePlanSchema
186+
);
187+
expect(result).toEqual({
188+
...mockClientData,
189+
pairwise: true,
190+
});
191+
expect(axiosMock.put).toHaveBeenCalledWith(
192+
`${ENV.URL_API.REGISTER}/client_id/${clientId}`,
193+
mockClientData,
194+
{
195+
headers: {
196+
Authorization: `Bearer ${token}`,
197+
'Plan-Api-Key': mockValidatePlanSchema.apiKeyValue,
198+
'Plan-Name': mockValidatePlanSchema.apiKeyId,
199+
},
200+
}
201+
);
202+
});
203+
154204
it('throws validation errors for invalid client data', async () => {
155205
const invalidData = { ...mockClientData, client_name: '' }; // Invalid client_name
156206
await expect(createOrUpdateClient(invalidData, token)).rejects.toThrow();
@@ -168,11 +218,136 @@ describe('createOrUpdateClient', () => {
168218
});
169219

170220
it('throws a generic error if the API call fails unexpectedly', async () => {
171-
const mockError = { isAxiosError: false, message: 'Unexpected error' };
221+
const mockError = { isAxiosError: false, message: unexpectedError };
172222
axiosMock.post.mockRejectedValueOnce(mockError);
173223

174224
await expect(createOrUpdateClient(mockClientData, token)).rejects.toThrow(
175-
'Unexpected error'
225+
unexpectedError
226+
);
227+
});
228+
});
229+
describe('getPlanData', () => {
230+
const clientID = '0000000000000000000000000000000000000000000';
231+
232+
const mockPlanData = {
233+
api_keys: [
234+
{
235+
id: 'id1',
236+
name: 'plan1',
237+
},
238+
{
239+
id: 'id2',
240+
name: 'plan2',
241+
},
242+
],
243+
};
244+
245+
beforeEach(() => {
246+
vi.stubGlobal('window', {
247+
location: { search: `?client_id=${clientID}` },
248+
});
249+
});
250+
251+
afterEach(() => {
252+
vi.restoreAllMocks();
253+
});
254+
255+
it('fetches plan data successfully', async () => {
256+
axiosMock.get.mockResolvedValueOnce({
257+
data: mockPlanData,
258+
});
259+
260+
const result = await getPlanList('token');
261+
expect(result).toEqual(mockPlanData);
262+
});
263+
264+
it('throws a custom error if the fetch fails', async () => {
265+
axiosMock.get.mockRejectedValue({
266+
isAxiosError: true,
267+
response: { data: { status: 401, message: 'Unauthorized' } },
268+
});
269+
270+
await expect(getPlanList('token')).rejects.toThrow('Unauthorized');
271+
});
272+
273+
it('throws an error if the fetch fails', async () => {
274+
axiosMock.get.mockRejectedValue({
275+
isAxiosError: true,
276+
response: { data: { status: 401 } },
277+
});
278+
279+
await expect(getPlanList('token')).rejects.toThrow(
280+
'Failed to fetch plan data'
281+
);
282+
});
283+
it('throws a generic error if the fetch fails', async () => {
284+
const mockError = {
285+
isAxiosError: false,
286+
response: { data: { status: 401 } },
287+
};
288+
axiosMock.get.mockRejectedValue(mockError);
289+
290+
await expect(getPlanList('token')).rejects.toThrow(
291+
`An unknown error occurred ${JSON.stringify(mockError)}`
292+
);
293+
});
294+
});
295+
296+
describe('validateApiKeyPlan', () => {
297+
const mockValidateApiKeyData: ValidateApiKeySchema = {
298+
valid: true,
299+
};
300+
301+
const mockValidatePlan: ValidatePlanSchema = {
302+
apiKeyId: 'plan1',
303+
apiKeyValue: 'keyValue',
304+
};
305+
306+
const token = 'test-token';
307+
308+
it('validates key successfully', async () => {
309+
axiosMock.post.mockResolvedValueOnce({
310+
data: {
311+
...mockValidateApiKeyData,
312+
},
313+
});
314+
315+
const result = await validateApiKeyPlan(mockValidatePlan, token);
316+
expect(result).toEqual({
317+
...mockValidateApiKeyData,
318+
});
319+
expect(axiosMock.post).toHaveBeenCalledWith(
320+
ENV.URL_API.REGISTER + ENV.URL_API.VALIDATE_API_PLAN,
321+
{
322+
api_key_id: mockValidatePlan.apiKeyId,
323+
api_key_value: mockValidatePlan.apiKeyValue,
324+
},
325+
{ headers: { Authorization: `Bearer ${token}` } }
326+
);
327+
});
328+
329+
it('throws validation errors for invalid API key data', async () => {
330+
const invalidData = { ...mockValidatePlan, apiKeyValue: '' };
331+
await expect(validateApiKeyPlan(invalidData, token)).rejects.toThrow();
332+
});
333+
334+
it('throws an error if the API call fails', async () => {
335+
axiosMock.post.mockRejectedValueOnce({
336+
isAxiosError: true,
337+
response: { data: { message: 'Failed to validate API key' } },
338+
});
339+
340+
await expect(validateApiKeyPlan(mockValidatePlan, token)).rejects.toThrow(
341+
'Failed to validate API key'
342+
);
343+
});
344+
345+
it('throws a generic error if the API call fails unexpectedly', async () => {
346+
const mockError = { isAxiosError: false, message: unexpectedError };
347+
axiosMock.post.mockRejectedValueOnce(mockError);
348+
349+
await expect(validateApiKeyPlan(mockValidatePlan, token)).rejects.toThrow(
350+
unexpectedError
176351
);
177352
});
178353
});

0 commit comments

Comments
 (0)