Skip to content

Commit cdfeb82

Browse files
committed
Add option resolution for Azure provider
1 parent 24c9ac6 commit cdfeb82

4 files changed

Lines changed: 291 additions & 0 deletions

File tree

packages/openops/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export * from './lib/openops-analytics/requests-helpers';
6464
export * from './lib/azure/auth';
6565
export * from './lib/azure/regions';
6666
export * from './lib/azure/subscription/get-subscription-dropdown';
67+
export * from './lib/azure/subscription/get-subscriptions';
6768

6869
export * from './lib/axios-wrapper';
6970
export * from './lib/cloud-cli-common';
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
authenticateUserWithAzure,
3+
getAzureRegionsList,
4+
getAzureSubscriptionsList,
5+
} from '@openops/common';
6+
import {
7+
BenchmarkWizardOption,
8+
CustomAuthConnectionValue,
9+
REGION_IMAGE_LOGO_URL,
10+
} from '@openops/shared';
11+
import { appConnectionService } from '../../../app-connection/app-connection-service/app-connection-service';
12+
import {
13+
getAuthProviderLogoUrl,
14+
listConnections,
15+
} from '../../common-resolvers';
16+
import { throwValidationError } from '../../errors';
17+
import type { WizardContext } from '../../provider-adapter';
18+
19+
export async function resolveOptions(
20+
method: string,
21+
context: WizardContext,
22+
): Promise<BenchmarkWizardOption[]> {
23+
switch (method) {
24+
case 'listConnections':
25+
return listConnections(context);
26+
case 'getSubscriptionsList':
27+
return getSubscriptionsList(context);
28+
case 'getRegionsList':
29+
return getAzureRegionsList().map((region) => ({
30+
...region,
31+
imageLogoUrl: REGION_IMAGE_LOGO_URL,
32+
}));
33+
default:
34+
throwValidationError(`Unknown Azure wizard option method: ${method}`);
35+
}
36+
}
37+
38+
async function getSubscriptionsList(
39+
context: WizardContext,
40+
): Promise<BenchmarkWizardOption[]> {
41+
const connectionId = context.benchmarkConfiguration?.connection?.[0];
42+
if (!connectionId) {
43+
throwValidationError('Connection must be selected to list subscriptions');
44+
}
45+
46+
const connection = await appConnectionService.getOneOrThrow({
47+
id: connectionId,
48+
projectId: context.projectId,
49+
});
50+
51+
const credentials = (connection.value as CustomAuthConnectionValue)?.props;
52+
const tokenResult = await authenticateUserWithAzure(credentials);
53+
const subscriptions = await getAzureSubscriptionsList(
54+
tokenResult.access_token,
55+
);
56+
57+
const imageLogoUrl = await getAuthProviderLogoUrl(
58+
connection.authProviderKey,
59+
context.projectId,
60+
);
61+
62+
return subscriptions.map((sub) => ({
63+
id: sub.subscriptionId,
64+
displayName: sub.displayName,
65+
...(imageLogoUrl && { imageLogoUrl }),
66+
}));
67+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ProviderAdapter, WizardConfig } from '../../provider-adapter';
2+
import { resolveOptions } from './azure-option-resolver';
3+
import azureConfig from './azure.json';
4+
5+
export const azureProviderAdapter: ProviderAdapter = {
6+
config: azureConfig as WizardConfig,
7+
resolveOptions,
8+
};
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { REGION_IMAGE_LOGO_URL } from '@openops/shared';
2+
3+
import { resolveOptions } from '../../../../../src/app/benchmark/providers/azure/azure-option-resolver';
4+
5+
const mockListConnections = jest.fn();
6+
const mockAuthenticateUserWithAzure = jest.fn();
7+
const mockGetAzureSubscriptionsList = jest.fn();
8+
const mockGetAzureRegionsList = jest.fn();
9+
const mockGetAuthProviderMetadata = jest.fn();
10+
11+
jest.mock('../../../../../src/app/benchmark/common-resolvers', () => ({
12+
...jest.requireActual('../../../../../src/app/benchmark/common-resolvers'),
13+
listConnections: (
14+
...args: unknown[]
15+
): ReturnType<typeof mockListConnections> => mockListConnections(...args),
16+
}));
17+
18+
jest.mock('@openops/common', () => ({
19+
...jest.requireActual('@openops/common'),
20+
authenticateUserWithAzure: (
21+
...args: unknown[]
22+
): ReturnType<typeof mockAuthenticateUserWithAzure> =>
23+
mockAuthenticateUserWithAzure(...args),
24+
getAzureSubscriptionsList: (
25+
...args: unknown[]
26+
): ReturnType<typeof mockGetAzureSubscriptionsList> =>
27+
mockGetAzureSubscriptionsList(...args),
28+
getAzureRegionsList: (
29+
...args: unknown[]
30+
): ReturnType<typeof mockGetAzureRegionsList> =>
31+
mockGetAzureRegionsList(...args),
32+
}));
33+
34+
jest.mock(
35+
'../../../../../src/app/app-connection/connection-providers-resolver',
36+
() => ({
37+
getAuthProviderMetadata: (
38+
...args: unknown[]
39+
): ReturnType<typeof mockGetAuthProviderMetadata> =>
40+
mockGetAuthProviderMetadata(...args),
41+
}),
42+
);
43+
44+
const mockGetOneOrThrow = jest.fn();
45+
jest.mock(
46+
'../../../../../src/app/app-connection/app-connection-service/app-connection-service',
47+
() => ({
48+
appConnectionService: {
49+
getOneOrThrow: (
50+
...args: unknown[]
51+
): ReturnType<typeof mockGetOneOrThrow> => mockGetOneOrThrow(...args),
52+
},
53+
}),
54+
);
55+
56+
describe('resolveOptions (Azure)', () => {
57+
const projectId = 'project-123';
58+
const provider = 'azure';
59+
60+
beforeEach(() => {
61+
jest.clearAllMocks();
62+
});
63+
64+
it('delegates to listConnections and returns its result for listConnections', async () => {
65+
const options = [
66+
{
67+
id: 'conn-1',
68+
displayName: 'Connection 1',
69+
metadata: { authProviderKey: 'Azure' },
70+
},
71+
];
72+
mockListConnections.mockResolvedValue(options);
73+
74+
const result = await resolveOptions('listConnections', {
75+
projectId,
76+
provider,
77+
});
78+
79+
expect(mockListConnections).toHaveBeenCalledTimes(1);
80+
expect(mockListConnections).toHaveBeenCalledWith({ projectId, provider });
81+
expect(result).toEqual(options);
82+
});
83+
84+
it('throws when getSubscriptionsList is called without a selected connection', async () => {
85+
await expect(
86+
resolveOptions('getSubscriptionsList', {
87+
projectId,
88+
provider,
89+
}),
90+
).rejects.toThrow('Connection must be selected to list subscriptions');
91+
expect(mockGetOneOrThrow).not.toHaveBeenCalled();
92+
});
93+
94+
it('returns subscriptions for getSubscriptionsList', async () => {
95+
mockGetOneOrThrow.mockResolvedValue({
96+
authProviderKey: 'Azure',
97+
value: {
98+
type: 'CUSTOM_AUTH',
99+
props: {
100+
clientId: 'cid',
101+
clientSecret: 'secret',
102+
tenantId: 'tenant',
103+
},
104+
},
105+
});
106+
mockAuthenticateUserWithAzure.mockResolvedValue({
107+
access_token: 'token-1',
108+
});
109+
mockGetAzureSubscriptionsList.mockResolvedValue([
110+
{
111+
subscriptionId: 'sub-a',
112+
displayName: 'Sub A',
113+
},
114+
{
115+
subscriptionId: 'sub-b',
116+
displayName: 'Sub B',
117+
},
118+
]);
119+
mockGetAuthProviderMetadata.mockResolvedValue({
120+
authProviderKey: 'Azure',
121+
authProviderLogoUrl: '/blocks/azure.svg',
122+
});
123+
124+
const result = await resolveOptions('getSubscriptionsList', {
125+
projectId,
126+
provider,
127+
benchmarkConfiguration: { connection: ['conn-123'] },
128+
});
129+
130+
expect(mockGetOneOrThrow).toHaveBeenCalledWith({
131+
id: 'conn-123',
132+
projectId,
133+
});
134+
expect(mockAuthenticateUserWithAzure).toHaveBeenCalledWith({
135+
clientId: 'cid',
136+
clientSecret: 'secret',
137+
tenantId: 'tenant',
138+
});
139+
expect(mockGetAzureSubscriptionsList).toHaveBeenCalledWith('token-1');
140+
expect(result).toEqual([
141+
{
142+
id: 'sub-a',
143+
displayName: 'Sub A',
144+
imageLogoUrl: '/blocks/azure.svg',
145+
},
146+
{
147+
id: 'sub-b',
148+
displayName: 'Sub B',
149+
imageLogoUrl: '/blocks/azure.svg',
150+
},
151+
]);
152+
});
153+
154+
it('omits imageLogoUrl for getSubscriptionsList when auth metadata has no logo', async () => {
155+
mockGetOneOrThrow.mockResolvedValue({
156+
authProviderKey: 'Azure',
157+
value: {
158+
type: 'CUSTOM_AUTH',
159+
props: {
160+
clientId: 'cid',
161+
clientSecret: 'secret',
162+
tenantId: 'tenant',
163+
},
164+
},
165+
});
166+
mockAuthenticateUserWithAzure.mockResolvedValue({ access_token: 't' });
167+
mockGetAzureSubscriptionsList.mockResolvedValue([
168+
{ subscriptionId: 'sub-1', displayName: 'One' },
169+
]);
170+
mockGetAuthProviderMetadata.mockResolvedValue(undefined);
171+
172+
const result = await resolveOptions('getSubscriptionsList', {
173+
projectId,
174+
provider,
175+
benchmarkConfiguration: { connection: ['c1'] },
176+
});
177+
178+
expect(result).toEqual([{ id: 'sub-1', displayName: 'One' }]);
179+
});
180+
181+
it('delegates to getAzureRegionsList and returns options with imageLogoUrl for getRegionsList', async () => {
182+
const regionsList = [
183+
{ id: 'eastus', displayName: 'East US' },
184+
{ id: 'westus2', displayName: 'West US 2' },
185+
];
186+
mockGetAzureRegionsList.mockReturnValue(regionsList);
187+
188+
const result = await resolveOptions('getRegionsList', {
189+
projectId,
190+
provider,
191+
});
192+
193+
expect(mockGetAzureRegionsList).toHaveBeenCalledTimes(1);
194+
expect(mockListConnections).not.toHaveBeenCalled();
195+
expect(result).toEqual([
196+
{
197+
id: 'eastus',
198+
displayName: 'East US',
199+
imageLogoUrl: REGION_IMAGE_LOGO_URL,
200+
},
201+
{
202+
id: 'westus2',
203+
displayName: 'West US 2',
204+
imageLogoUrl: REGION_IMAGE_LOGO_URL,
205+
},
206+
]);
207+
});
208+
209+
it('throws with method name in message for unknown method', async () => {
210+
await expect(
211+
resolveOptions('unknownMethod', { projectId, provider }),
212+
).rejects.toThrow('Unknown Azure wizard option method: unknownMethod');
213+
expect(mockListConnections).not.toHaveBeenCalled();
214+
});
215+
});

0 commit comments

Comments
 (0)