Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/client/power-pages/actions-hub/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ export const Constants = {
ACTIONS_HUB_PREVIEW_SITE_FAILED: "ActionsHubPreviewSiteFailed",
ACTIONS_HUB_CREATE_AUTH_PROFILE_CALLED: "ActionsHubCreateAuthProfileCalled",
ACTIONS_HUB_CREATE_AUTH_PROFILE_FAILED: "ActionsHubCreateAuthProfileFailed",
ACTIONS_HUB_AUTH_SYNCED_FROM_PAC_CLI: "ActionsHubAuthSyncedFromPacCli",
ACTIONS_HUB_FETCH_WEBSITES_CALLED: "ActionsHubFetchWebsitesCalled",
ACTIONS_HUB_FETCH_WEBSITES_FAILED: "ActionsHubFetchWebsitesFailed",
ACTIONS_HUB_REVEAL_IN_OS_CALLED: "ActionsHubRevealInOSCalled",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { authenticateUserInVSCode } from "../../../../common/services/Authentica
import { createAuthProfileExp } from "../../../../common/utilities/PacAuthUtil";
import PacContext from "../../../pac/PacContext";
import { PacWrapper } from "../../../pac/PacWrapper";
import { extractAuthInfo, extractOrgInfo } from "../../commonUtility";
import { Constants } from "../Constants";
import { traceError, traceInfo } from "../TelemetryHelper";

Expand All @@ -22,6 +23,12 @@ export const createNewAuthProfile = async (pacWrapper: PacWrapper): Promise<void
return;
}

// PacContext may not be populated yet - check PAC CLI directly for active auth
if (await syncAuthFromPacCli(pacWrapper)) {
await authenticateUserInVSCode();
return;
}

const pacAuthCreateOutput = await createAuthProfileExp(pacWrapper);
if (pacAuthCreateOutput && pacAuthCreateOutput.Status === SUCCESS) {
const results = pacAuthCreateOutput.Results;
Expand Down Expand Up @@ -58,3 +65,45 @@ export const createNewAuthProfile = async (pacWrapper: PacWrapper): Promise<void
);
}
};

/**
* Checks PAC CLI for an active auth profile and syncs it to PacContext.
* This handles the case where the user has already authenticated via PAC CLI
* but PacContext has not yet been populated (e.g., file watcher hasn't fired).
* @returns true if a valid PAC CLI auth profile was found and synced, false otherwise.
*/
export const syncAuthFromPacCli = async (pacWrapper: PacWrapper): Promise<boolean> => {
try {
const pacActiveAuth = await pacWrapper.activeAuth();
if (pacActiveAuth && pacActiveAuth.Status === SUCCESS) {
const authInfo = extractAuthInfo(pacActiveAuth.Results);
if (authInfo.OrganizationFriendlyName) {
let orgInfo = null;
try {
const pacActiveOrg = await pacWrapper.activeOrg();
if (pacActiveOrg && pacActiveOrg.Status === SUCCESS) {
orgInfo = extractOrgInfo(pacActiveOrg.Results);
}
} catch (error) {
// Org info fetch failed, log for diagnostics but continue with auth info only
traceInfo(Constants.EventNames.ACTIONS_HUB_AUTH_SYNCED_FROM_PAC_CLI, {
methodName: syncAuthFromPacCli.name,
orgFetchFailed: true,
errorMessage: error instanceof Error ? error.message : String(error)
});
}
PacContext.setContext(authInfo, orgInfo);
traceInfo(Constants.EventNames.ACTIONS_HUB_AUTH_SYNCED_FROM_PAC_CLI, { methodName: syncAuthFromPacCli.name });
return true;
}
}
} catch (error) {
// PAC CLI auth not available, log for diagnostics and continue with auth profile creation flow
traceInfo(Constants.EventNames.ACTIONS_HUB_AUTH_SYNCED_FROM_PAC_CLI, {
methodName: syncAuthFromPacCli.name,
pacAuthCheckFailed: true,
errorMessage: error instanceof Error ? error.message : String(error)
});
}
return false;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import { createNewAuthProfile } from '../../../../../power-pages/actions-hub/handlers/CreateNewAuthProfileHandler';
import { createNewAuthProfile, syncAuthFromPacCli } from '../../../../../power-pages/actions-hub/handlers/CreateNewAuthProfileHandler';
import { PacWrapper } from '../../../../../pac/PacWrapper';
import * as authProvider from '../../../../../../common/services/AuthenticationProvider';
import * as PacAuthUtil from '../../../../../../common/utilities/PacAuthUtil';
import * as CommonUtility from '../../../../../power-pages/commonUtility';
import PacContext from '../../../../../pac/PacContext';
import * as TelemetryHelper from '../../../../../power-pages/actions-hub/TelemetryHelper';
import { AuthInfo, CloudInstance, EnvironmentType, OrgInfo, PacAuthWhoOutput, PacOrgWhoOutput } from '../../../../../pac/PacTypes';

describe('CreateNewAuthProfileHandler', () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -99,5 +101,133 @@ describe('CreateNewAuthProfileHandler', () => {
expect(traceErrorStub.calledOnce).to.be.true;
expect(traceErrorStub.firstCall.args[0]).to.equal('ActionsHubCreateAuthProfileFailed');
});

it('should sync auth from PAC CLI and authenticate in VS Code when PAC has active auth but PacContext is not populated', async () => {
const mockAuthResults = [
{ Key: 'OrganizationFriendlyName', Value: 'Test Organization' },
{ Key: 'EntraIdObjectId', Value: 'test-object-id' }
];
mockPacWrapper.activeAuth.resolves({ Status: 'Success', Results: mockAuthResults } as unknown as PacAuthWhoOutput);
mockPacWrapper.activeOrg.resolves({
Status: 'Success',
Results: { OrgId: 'org-id', UniqueName: 'testorg', FriendlyName: 'Test Organization', OrgUrl: 'https://test.crm.dynamics.com', UserEmail: 'test@test.com', UserId: 'user-id', EnvironmentId: 'env-id' }
} as unknown as PacOrgWhoOutput);
sandbox.stub(CommonUtility, 'extractAuthInfo').returns({
OrganizationFriendlyName: 'Test Organization',
EntraIdObjectId: 'test-object-id'
} as unknown as AuthInfo);
sandbox.stub(CommonUtility, 'extractOrgInfo').returns({
OrgId: 'org-id',
OrgUrl: 'https://test.crm.dynamics.com'
} as OrgInfo);
const setContextStub = sandbox.stub(PacContext, 'setContext');

await createNewAuthProfile(mockPacWrapper);

expect(mockPacWrapper.activeAuth.calledOnce).to.be.true;
expect(setContextStub.calledOnce).to.be.true;
expect(mockAuthenticationInVsCode.calledOnce).to.be.true;
expect(mockCreateAuthProfileExp.called).to.be.false;
});

it('should fall through to createAuthProfileExp when PAC CLI has no active auth', async () => {
mockPacWrapper.activeAuth.resolves({ Status: 'Failed', Results: [] } as unknown as PacAuthWhoOutput);
mockCreateAuthProfileExp.resolves({ Status: 'Failed', Results: null });

await createNewAuthProfile(mockPacWrapper);

expect(mockPacWrapper.activeAuth.calledOnce).to.be.true;
expect(mockCreateAuthProfileExp.calledOnce).to.be.true;
});

it('should fall through to createAuthProfileExp when PAC CLI activeAuth throws', async () => {
mockPacWrapper.activeAuth.rejects(new Error('PAC CLI error'));
mockCreateAuthProfileExp.resolves({ Status: 'Failed', Results: null });

await createNewAuthProfile(mockPacWrapper);

expect(mockPacWrapper.activeAuth.calledOnce).to.be.true;
expect(mockCreateAuthProfileExp.calledOnce).to.be.true;
});
});

describe('syncAuthFromPacCli', () => {
let mockPacWrapper: sinon.SinonStubbedInstance<PacWrapper>;

beforeEach(() => {
mockPacWrapper = sandbox.createStubInstance(PacWrapper);
});

it('should return true and set context when PAC CLI has valid auth with OrganizationFriendlyName', async () => {
const mockAuthResults = [
{ Key: 'OrganizationFriendlyName', Value: 'Test Organization' }
];
mockPacWrapper.activeAuth.resolves({ Status: 'Success', Results: mockAuthResults } as unknown as PacAuthWhoOutput);
mockPacWrapper.activeOrg.resolves({
Status: 'Success',
Results: { OrgId: 'org-id', UniqueName: 'testorg', FriendlyName: 'Test Org', OrgUrl: 'https://test.crm.dynamics.com', UserEmail: 'test@test.com', UserId: 'user-id', EnvironmentId: 'env-id' }
} as unknown as PacOrgWhoOutput);
sandbox.stub(CommonUtility, 'extractAuthInfo').returns({
OrganizationFriendlyName: 'Test Organization'
} as unknown as AuthInfo);
sandbox.stub(CommonUtility, 'extractOrgInfo').returns({
OrgId: 'org-id', OrgUrl: 'https://test.crm.dynamics.com'
} as OrgInfo);
const setContextStub = sandbox.stub(PacContext, 'setContext');

const result = await syncAuthFromPacCli(mockPacWrapper);

expect(result).to.be.true;
expect(setContextStub.calledOnce).to.be.true;
});

it('should return false when PAC CLI has no active auth', async () => {
mockPacWrapper.activeAuth.resolves({ Status: 'Failed', Results: [] } as unknown as PacAuthWhoOutput);

const result = await syncAuthFromPacCli(mockPacWrapper);

expect(result).to.be.false;
});

it('should return false when PAC CLI auth has empty OrganizationFriendlyName', async () => {
const mockAuthResults = [
{ Key: 'OrganizationFriendlyName', Value: '' }
];
mockPacWrapper.activeAuth.resolves({ Status: 'Success', Results: mockAuthResults } as unknown as PacAuthWhoOutput);
sandbox.stub(CommonUtility, 'extractAuthInfo').returns({
OrganizationFriendlyName: ''
} as unknown as AuthInfo);

const result = await syncAuthFromPacCli(mockPacWrapper);

expect(result).to.be.false;
});

it('should return false when PAC CLI activeAuth throws', async () => {
mockPacWrapper.activeAuth.rejects(new Error('PAC CLI error'));

const result = await syncAuthFromPacCli(mockPacWrapper);

expect(result).to.be.false;
});

it('should still succeed when activeOrg fails', async () => {
const mockAuthResults = [
{ Key: 'OrganizationFriendlyName', Value: 'Test Organization' }
];
mockPacWrapper.activeAuth.resolves({ Status: 'Success', Results: mockAuthResults } as unknown as PacAuthWhoOutput);
mockPacWrapper.activeOrg.rejects(new Error('Org fetch failed'));
sandbox.stub(CommonUtility, 'extractAuthInfo').returns({
OrganizationFriendlyName: 'Test Organization'
} as unknown as AuthInfo);
const setContextStub = sandbox.stub(PacContext, 'setContext');

const result = await syncAuthFromPacCli(mockPacWrapper);

expect(result).to.be.true;
expect(setContextStub.calledOnce).to.be.true;
// Should be called with authInfo and null orgInfo
expect(setContextStub.firstCall.args[1]).to.be.null;
});
});
});
Loading