Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/wide-peaches-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/axios-extension': patch
'@sap-ux/generator-adp': patch
---

fix(adp): Problems with login in the Replace OData Service change editor.
1 change: 1 addition & 0 deletions packages/adp-tooling/src/abap/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export async function getFlexUISupportedSystem(
*
* @param {AbapServiceProvider} provider - Instance of the ABAP provider.
* @param {ToolsLogger} logger - The logger instance.
* @throws Throws exceptions only when the ADT api is used.
* @returns {string | undefined} System UI5 version.
*/
export async function getSystemUI5Version(
Expand Down
31 changes: 27 additions & 4 deletions packages/generator-adp/src/base/questions/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import { isAppStudio } from '@sap-ux/btp-utils';
import type { ToolsLogger } from '@sap-ux/logger';
import type { AbapTarget } from '@sap-ux/system-access';
import { validateEmptyString } from '@sap-ux/project-input-validator';
import { getConfiguredProvider, getSystemUI5Version } from '@sap-ux/adp-tooling';
import { getConfiguredProvider } from '@sap-ux/adp-tooling';
import type { InputQuestion, PasswordQuestion, YUIQuestion } from '@sap-ux/inquirer-common';

import { t } from '../../utils/i18n';
import type { Credentials } from '../../types';
import { configPromptNames } from '../../app/types';
import type { LayeredRepositoryService } from '@sap-ux/axios-extension';

interface ClientCredentials {
system: string;
client: string;
username: string;
password: string;
}

/**
* Returns the username prompt.
Expand Down Expand Up @@ -66,15 +74,14 @@ function getPasswordPrompt(abapTarget: AbapTarget, logger: ToolsLogger): Passwor
}

try {
const options = {
const credentials = {
system,
client: abapTarget.client ?? '',
username: answers.username,
password: value
};

const abapProvider = await getConfiguredProvider(options, logger);
await getSystemUI5Version(abapProvider, logger);
await assertAuthenticated(credentials, logger);

return true;
} catch (e) {
Expand All @@ -83,3 +90,19 @@ function getPasswordPrompt(abapTarget: AbapTarget, logger: ToolsLogger): Passwor
}
};
}

/**
* Helper function which asserts whether a client is authenticated on an ABAP system or throws.
* Since we do not have a dedicated api call to detect if a client is authenticated we use the
* {@link LayeredRepositoryService.getSystemInfo} call which is a protected one.
*
* @param {ClientCredentials} credentials - Object containing client credentials to a specific ABAP system.
* @param {ToolsLogger} logger - The logger instance.
* @returns {Promise<void>} A promise resolved if the client is authenticated, otherwise rejected with
* an error.
*/
async function assertAuthenticated(credentials: ClientCredentials, logger: ToolsLogger): Promise<void> {
const abapProvider = await getConfiguredProvider(credentials, logger);
const layeredRepositoryService = abapProvider.getLayeredRepository();
await layeredRepositoryService.getSystemInfo();
}
26 changes: 6 additions & 20 deletions packages/generator-adp/src/base/sub-gen-auth-base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path';
import { MessageType, Prompts } from '@sap-devx/yeoman-ui-types';
import { Prompts } from '@sap-devx/yeoman-ui-types';

import { isAppStudio } from '@sap-ux/btp-utils';
import type { Manifest } from '@sap-ux/project-access';
Expand All @@ -13,7 +13,7 @@ import SubGeneratorBase from './sub-gen-base';
import type { GeneratorOpts } from '../utils/opts';
import type { GeneratorTypes, Credentials } from '../types';
import { getCredentialsPrompts } from './questions/credentials';
import { getSubGenAuthPages, getSubGenErrorPage } from '../utils/steps';
import { getSubGenAuthPages } from '../utils/steps';

/**
* Base class for *sub* generators that need authentication handling.
Expand Down Expand Up @@ -68,14 +68,10 @@ export default class SubGeneratorWithAuthBase extends SubGeneratorBase {
super(args, opts, type);
this.generatorType = type;

try {
if (opts.data) {
this.projectPath = opts.data.path;
}
this.vscode = opts.vscode;
} catch (e) {
this.validationError = e as Error;
if (opts.data) {
this.projectPath = opts.data.path;
}
this.vscode = opts.vscode;
}

/**
Expand All @@ -85,23 +81,13 @@ export default class SubGeneratorWithAuthBase extends SubGeneratorBase {
protected async onInit(): Promise<void> {
await initI18n();

if (this.validationError) {
this._registerPrompts(new Prompts(getSubGenErrorPage(this.generatorType)));
} else {
this._registerPrompts(new Prompts(getSubGenAuthPages(this.generatorType, this.system)));
}

this.systemLookup = new SystemLookup(this.logger);
const adpConfig = await getAdpConfig(this.projectPath, path.join(this.projectPath, 'ui5.yaml'));
this.abapTarget = adpConfig.target;
this.system = (isAppStudio() ? this.abapTarget.destination : this.abapTarget.url) ?? '';
this.logger.log(`Successfully retrieved abap target\n${JSON.stringify(this.abapTarget, null, 2)}`);

if (this.validationError) {
this.appWizard.showError(this.validationError.message, MessageType.notification);
await this.handleRuntimeCrash(this.validationError.message);
return;
}
this._registerPrompts(new Prompts(getSubGenAuthPages(this.generatorType, this.system)));

try {
this.requiresAuth = await this.systemLookup.getSystemRequiresAuth(this.system);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ToolsLogger } from '@sap-ux/logger';
import type { AbapTarget } from '@sap-ux/system-access';
import type { AbapServiceProvider } from '@sap-ux/axios-extension';
import { validateEmptyString } from '@sap-ux/project-input-validator';
import { getConfiguredProvider, getSystemUI5Version } from '@sap-ux/adp-tooling';
import { getConfiguredProvider } from '@sap-ux/adp-tooling';

import type { Credentials } from '../../../../src/types';
import { initI18n, t } from '../../../../src/utils/i18n';
Expand All @@ -22,14 +22,12 @@ jest.mock('@sap-ux/project-input-validator', () => ({

jest.mock('@sap-ux/adp-tooling', () => ({
...jest.requireActual('@sap-ux/adp-tooling'),
getConfiguredProvider: jest.fn(),
getSystemUI5Version: jest.fn()
getConfiguredProvider: jest.fn()
}));

const mockIsAppStudio = isAppStudio as jest.MockedFunction<typeof isAppStudio>;
const mockValidateEmptyString = validateEmptyString as jest.MockedFunction<typeof validateEmptyString>;
const mockGetConfiguredProvider = getConfiguredProvider as jest.MockedFunction<typeof getConfiguredProvider>;
const mockGetSystemUI5Version = getSystemUI5Version as jest.MockedFunction<typeof getSystemUI5Version>;

describe('Credentials Prompts', () => {
let mockLogger: ToolsLogger;
Expand Down Expand Up @@ -112,10 +110,15 @@ describe('Credentials Prompts', () => {

describe('Password Prompt', () => {
let passwordPrompt: any;
let mockGetSystemInfo: jest.Mock;

beforeEach(() => {
const result = getCredentialsPrompts(mockAbapTarget, mockLogger);
passwordPrompt = result[1];
mockGetSystemInfo = jest.fn();
mockGetConfiguredProvider.mockResolvedValue({
getLayeredRepository: () => ({ getSystemInfo: mockGetSystemInfo })
} as unknown as AbapServiceProvider);
});

it('should have correct password prompt structure', () => {
Expand All @@ -136,8 +139,7 @@ describe('Credentials Prompts', () => {
mockIsAppStudio.mockReturnValue(false);
mockAbapTarget.url = 'some-system';
mockValidateEmptyString.mockReturnValue(true);
mockGetConfiguredProvider.mockResolvedValue({} as AbapServiceProvider);
mockGetSystemUI5Version.mockResolvedValue('1.136.1');
mockGetSystemInfo.mockResolvedValue({});

const prompts = getCredentialsPrompts(mockAbapTarget, mockLogger);
const passwordPrompt = prompts[1];
Expand All @@ -157,15 +159,15 @@ describe('Credentials Prompts', () => {
},
mockLogger
);
expect(mockGetSystemInfo).toHaveBeenCalled();
expect(result).toBe(true);
});

it('should use destination when in AppStudio', async () => {
mockIsAppStudio.mockReturnValue(true);
mockAbapTarget.destination = 'SYS_010';
mockValidateEmptyString.mockReturnValue(true);
mockGetConfiguredProvider.mockResolvedValue({} as AbapServiceProvider);
mockGetSystemUI5Version.mockResolvedValue('1.136.1');
mockGetSystemInfo.mockResolvedValue({});

const prompts = getCredentialsPrompts(mockAbapTarget, mockLogger);
const passwordPrompt = prompts[1];
Expand All @@ -185,6 +187,7 @@ describe('Credentials Prompts', () => {
},
mockLogger
);
expect(mockGetSystemInfo).toHaveBeenCalled();
expect(result).toBe(true);
});

Expand Down Expand Up @@ -227,7 +230,7 @@ describe('Credentials Prompts', () => {
statusText: 'Unauthorized'
}
};
mockGetConfiguredProvider.mockRejectedValue(mockError);
mockGetSystemInfo.mockRejectedValue(mockError);

const prompts = getCredentialsPrompts(mockAbapTarget, mockLogger);
const passwordPrompt = prompts[1];
Expand Down
Loading