diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx index 431309827d4..ea9664cb734 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/WorkflowAndArtifacts.tsx @@ -3,7 +3,7 @@ import type { CallbackInfo, ConnectionsData, Note, ParametersData, Workflow } fr import { Artifact } from '../Models/Workflow'; import { validateResourceId } from '../Utilities/resourceUtilities'; import { convertDesignerWorkflowToConsumptionWorkflow } from './ConsumptionSerializationHelpers'; -import { getHostConfig, getReactQueryClient, runsQueriesKeys, type AllCustomCodeFiles } from '@microsoft/logic-apps-designer'; +import { getHostConfig, getReactQueryClient, runsQueriesKeys } from '@microsoft/logic-apps-designer'; import { CustomCodeService, LogEntryLevel, LoggerService, equals, getAppFileForFileExtension } from '@microsoft/logic-apps-shared'; import type { AgentQueryParams, AgentURL, LogicAppsV2, McpServer, VFSObject } from '@microsoft/logic-apps-shared'; import axios from 'axios'; @@ -11,7 +11,7 @@ import jwt_decode from 'jwt-decode'; import { useQuery } from '@tanstack/react-query'; import { isSuccessResponse } from './HttpClient'; import { fetchFileData, fetchFilesFromFolder } from './vfsService'; -import type { CustomCodeFileNameMapping, ServerNotificationData } from '@microsoft/logic-apps-designer'; +import type { CustomCodeFileNameMapping, ServerNotificationData, AllCustomCodeFiles } from '@microsoft/logic-apps-designer'; import { HybridAppUtility, hybridApiVersion } from '../Utilities/HybridAppUtilities'; import type { HostingPlanTypes } from '../../../state/workflowLoadingSlice'; import { ArmParser } from '../Utilities/ArmParser'; @@ -292,21 +292,24 @@ export const listCallbackUrl = async ( }; // Helper function to fetch A2A authentication key -const fetchA2AAuthKey = async (siteResourceId: string, workflowName: string, isDraftMode?: boolean) => { +export const fetchA2AAuthKey = async (siteResourceId: string, workflowName: string, isDraftMode?: boolean) => { const currentDate: Date = new Date(); + const data = { + expiry: new Date(currentDate.getTime() + 86400000).toISOString(), + keyType: 'Primary', + }; + const authToken = { + Authorization: `Bearer ${environment.armToken}`, + }; + const endpoint = `${baseUrl}${siteResourceId}/hostruntime/runtime/webhooks/workflow/api/management/workflows/${workflowName}/${isDraftMode ? 'listDraftApiKeys' : 'listApiKeys'}`; - const response = await axios.post( - `${baseUrl}${siteResourceId}/hostruntime/runtime/webhooks/workflow/api/management/workflows/${workflowName}/${isDraftMode ? 'listDraftApiKeys' : 'listApiKeys'}?api-version=2018-11-01`, - { - expiry: new Date(currentDate.getTime() + 86400000).toISOString(), - keyType: 'Primary', - }, - { - headers: { - Authorization: `Bearer ${environment.armToken}`, - }, - } - ); + if (HybridAppUtility.isHybridLogicApp(siteResourceId)) { + return HybridAppUtility.postProxy(endpoint, data, authToken); + } + + const response = await axios.post(`${endpoint}?api-version=2018-11-01`, data, { + headers: authToken, + }); return response.data; }; diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/__test__/WorkflowAndArtifacts.test.ts b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/__test__/WorkflowAndArtifacts.test.ts new file mode 100644 index 00000000000..09c63c22e1a --- /dev/null +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Services/__test__/WorkflowAndArtifacts.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import axios from 'axios'; +import { fetchA2AAuthKey } from '../WorkflowAndArtifacts'; +import { HybridAppUtility } from '../../Utilities/HybridAppUtilities'; + +// Mock axios +vi.mock('axios'); + +// Mock the environment +vi.mock('../../../../../environments/environment', () => ({ + environment: { + armToken: 'test-arm-token', + }, +})); + +describe('fetchA2AAuthKey', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-02-23T12:00:00.000Z')); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.useRealTimers(); + }); + + describe('when siteResourceId is a hybrid logic app', () => { + const hybridSiteResourceId = '/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.App/containerApps/myHybridApp'; + const workflowName = 'myWorkflow'; + + it('should call HybridAppUtility.postProxy with correct parameters', async () => { + const mockResponse = { key: 'hybrid-api-key', endpoint: 'https://example.com' }; + const postProxySpy = vi.spyOn(HybridAppUtility, 'postProxy').mockResolvedValue(mockResponse); + + const result = await fetchA2AAuthKey(hybridSiteResourceId, workflowName); + + expect(postProxySpy).toHaveBeenCalledWith( + 'https://management.azure.com/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.App/containerApps/myHybridApp/hostruntime/runtime/webhooks/workflow/api/management/workflows/myWorkflow/listApiKeys', + { + expiry: '2026-02-24T12:00:00.000Z', // 24 hours later + keyType: 'Primary', + }, + { + Authorization: 'Bearer test-arm-token', + } + ); + expect(result).toEqual(mockResponse); + }); + + it('should use listDraftApiKeys endpoint when isDraftMode is true', async () => { + const mockResponse = { key: 'draft-api-key' }; + const postProxySpy = vi.spyOn(HybridAppUtility, 'postProxy').mockResolvedValue(mockResponse); + + await fetchA2AAuthKey(hybridSiteResourceId, workflowName, true); + + expect(postProxySpy).toHaveBeenCalledWith(expect.stringContaining('/listDraftApiKeys'), expect.any(Object), expect.any(Object)); + }); + + it('should NOT call axios.post directly for hybrid apps', async () => { + vi.spyOn(HybridAppUtility, 'postProxy').mockResolvedValue({ key: 'test' }); + + await fetchA2AAuthKey(hybridSiteResourceId, workflowName); + + expect(axios.post).not.toHaveBeenCalled(); + }); + }); + + describe('when siteResourceId is a standard logic app', () => { + const standardSiteResourceId = '/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Web/sites/myStandardApp'; + const workflowName = 'myWorkflow'; + + it('should call axios.post with api-version query parameter', async () => { + const mockResponse = { data: { key: 'standard-api-key', endpoint: 'https://example.com' } }; + (axios.post as any).mockResolvedValue(mockResponse); + + const result = await fetchA2AAuthKey(standardSiteResourceId, workflowName); + + expect(axios.post).toHaveBeenCalledWith( + 'https://management.azure.com/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Web/sites/myStandardApp/hostruntime/runtime/webhooks/workflow/api/management/workflows/myWorkflow/listApiKeys?api-version=2018-11-01', + { + expiry: '2026-02-24T12:00:00.000Z', + keyType: 'Primary', + }, + { + headers: { + Authorization: 'Bearer test-arm-token', + }, + } + ); + expect(result).toEqual(mockResponse.data); + }); + + it('should use listDraftApiKeys endpoint when isDraftMode is true', async () => { + const mockResponse = { data: { key: 'draft-api-key' } }; + (axios.post as any).mockResolvedValue(mockResponse); + + await fetchA2AAuthKey(standardSiteResourceId, workflowName, true); + + expect(axios.post).toHaveBeenCalledWith( + expect.stringContaining('/listDraftApiKeys?api-version=2018-11-01'), + expect.any(Object), + expect.any(Object) + ); + }); + + it('should NOT call HybridAppUtility.postProxy for standard apps', async () => { + const postProxySpy = vi.spyOn(HybridAppUtility, 'postProxy'); + (axios.post as any).mockResolvedValue({ data: { key: 'test' } }); + + await fetchA2AAuthKey(standardSiteResourceId, workflowName); + + expect(postProxySpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/HybridAppUtilities.ts b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/HybridAppUtilities.ts index 97ee56c621f..223cd2fcc38 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/HybridAppUtilities.ts +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/Utilities/HybridAppUtilities.ts @@ -27,18 +27,8 @@ export class HybridAppUtility { } public static async postProxy(uri: string, data: any, headers: Record, params?: Record): Promise { - const splitUri = uri.split('/hostruntime/'); - const appName = splitUri[0].split('/').pop(); - return ( - await axios.post(`${splitUri[0]}/providers/Microsoft.App/logicapps/${appName}/invoke?api-version=${hybridApiVersion}`, data, { - headers: { - ...headers, - 'x-ms-logicapps-proxy-path': `/${splitUri[1]}`, - 'x-ms-logicapps-proxy-method': 'POST', - }, - params, - }) - ).data; + const response = await HybridAppUtility.postProxyResponse(uri, data, headers, params); + return response.data; } public static async postProxyResponse(uri: string, data: any, headers: Record, params?: Record) { @@ -48,7 +38,7 @@ export class HybridAppUtility { return await axios.post(`${baseUri}/providers/Microsoft.App/logicapps/${appName}/invoke?api-version=${hybridApiVersion}`, data, { headers: { ...headers, - 'x-ms-logicapps-proxy-path': `${path}/`, + 'x-ms-logicapps-proxy-path': `/${path}`, 'x-ms-logicapps-proxy-method': 'POST', }, params, diff --git a/vitest.workspace.ts b/vitest.workspace.ts index b64d2799682..37a30243401 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1 +1 @@ -export default ['libs/']; +export default ['libs/', 'apps/Standalone/'];