diff --git a/packages/btp-utils/src/destination.ts b/packages/btp-utils/src/destination.ts index 689f96db35..81576bb2f4 100644 --- a/packages/btp-utils/src/destination.ts +++ b/packages/btp-utils/src/destination.ts @@ -76,6 +76,9 @@ export interface Destination extends Partial { Authentication: string; ProxyType: string; Description: string; + /** + * N.B. Not the host but the full destination URL property! + */ Host: string; } @@ -224,3 +227,16 @@ export function isS4HC(destination: Destination): boolean { destination.ProxyType === ProxyType.INTERNET ); } + +/** + * Checks if the destination attributes WebIDEUsage is configured with odata_abap. + * + * @param destination destination configuration properties + * @returns true, if this destination has the the 'odata_abap' attribute set + */ +export function isAbapODataDestination(destination: Destination): boolean { + return ( + !!destination.WebIDEUsage?.includes(WebIDEUsage.ODATA_ABAP) && + !destination.WebIDEUsage?.includes(WebIDEUsage.ODATA_GENERIC) + ); +} diff --git a/packages/odata-service-inquirer/jest.config.js b/packages/odata-service-inquirer/jest.config.js index fc001d58ed..9a7ed255f3 100644 --- a/packages/odata-service-inquirer/jest.config.js +++ b/packages/odata-service-inquirer/jest.config.js @@ -4,4 +4,5 @@ config.snapshotFormat = { escapeString: false, printBasicPrototype: false }; +config.collectCoverage = false; module.exports = config; diff --git a/packages/odata-service-inquirer/package.json b/packages/odata-service-inquirer/package.json index 2b8e68bd7d..e8dad5199d 100644 --- a/packages/odata-service-inquirer/package.json +++ b/packages/odata-service-inquirer/package.json @@ -54,9 +54,7 @@ "@types/inquirer-autocomplete-prompt": "2.0.1", "@types/inquirer": "8.2.6", "@types/lodash": "4.14.202", - "jest-extended": "3.2.4", - "lodash": "4.17.21" - }, + "jest-extended": "3.2.4" }, "engines": { "node": ">=18.x" } diff --git a/packages/odata-service-inquirer/src/error-handler/error-handler.ts b/packages/odata-service-inquirer/src/error-handler/error-handler.ts index 2764121737..00116b1682 100644 --- a/packages/odata-service-inquirer/src/error-handler/error-handler.ts +++ b/packages/odata-service-inquirer/src/error-handler/error-handler.ts @@ -45,7 +45,8 @@ export enum ERROR_TYPE { DESTINATION_NOT_FOUND = 'DESTINATION_NOT_FOUND', DESTINATION_MISCONFIGURED = 'DESTINATION_MISCONFIGURED', NO_V2_SERVICES = 'NO_V2_SERVICES', - NO_V4_SERVICES = 'NO_V4_SERVICES' + NO_V4_SERVICES = 'NO_V4_SERVICES', + BAD_REQUEST = 'BAD_REQUEST' } // Used to match regex expressions to error messages, etc. providing a way to return a consistent @@ -90,7 +91,8 @@ export const ERROR_MAP: Record = { [ERROR_TYPE.DESTINATION_NOT_FOUND]: [], [ERROR_TYPE.DESTINATION_MISCONFIGURED]: [], [ERROR_TYPE.NO_V2_SERVICES]: [], - [ERROR_TYPE.NO_V4_SERVICES]: [] + [ERROR_TYPE.NO_V4_SERVICES]: [], + [ERROR_TYPE.BAD_REQUEST]: [/400/] }; type ValidationLinkOrString = string | ValidationLink; @@ -124,7 +126,7 @@ export class ErrorHandler { [ERROR_TYPE.CERT_SELF_SIGNED_CERT_IN_CHAIN]: t('errors.urlCertValidationError', { certErrorReason: t('texts.anUntrustedRootCert') }), - [ERROR_TYPE.AUTH]: t('errors.authenticationFailed', { error }), + [ERROR_TYPE.AUTH]: t('errors.authenticationFailed', { error: (error as Error)?.message || error }), [ERROR_TYPE.AUTH_TIMEOUT]: t('errors.authenticationTimeout'), [ERROR_TYPE.TIMEOUT]: t('errors.timeout, { error }'), [ERROR_TYPE.INVALID_URL]: t('errors.invalidUrl'), @@ -149,7 +151,8 @@ export class ErrorHandler { [ERROR_TYPE.DESTINATION_BAD_GATEWAY_503]: t('errors.destinationUnavailable'), [ERROR_TYPE.REDIRECT]: t('errors.redirectError'), [ERROR_TYPE.NO_SUCH_HOST]: t('errors.noSuchHostError'), - [ERROR_TYPE.NO_ABAP_ENVS]: t('errors.abapEnvsUnavailable') + [ERROR_TYPE.NO_ABAP_ENVS]: t('errors.abapEnvsUnavailable'), + [ERROR_TYPE.BAD_REQUEST]: t('errors.badRequest') }); /** @@ -188,7 +191,8 @@ export class ErrorHandler { [ERROR_TYPE.ODATA_URL_NOT_FOUND]: undefined, [ERROR_TYPE.INTERNAL_SERVER_ERROR]: undefined, [ERROR_TYPE.NO_V2_SERVICES]: undefined, - [ERROR_TYPE.TIMEOUT]: undefined + [ERROR_TYPE.TIMEOUT]: undefined, + [ERROR_TYPE.BAD_REQUEST]: undefined }; return errorToHelp[errorType]; }; diff --git a/packages/odata-service-inquirer/src/index.ts b/packages/odata-service-inquirer/src/index.ts index 8a951edce3..a82a40cb47 100644 --- a/packages/odata-service-inquirer/src/index.ts +++ b/packages/odata-service-inquirer/src/index.ts @@ -6,7 +6,7 @@ import autocomplete from 'inquirer-autocomplete-prompt'; import { ERROR_TYPE, ErrorHandler } from './error-handler/error-handler'; import { initI18nOdataServiceInquirer } from './i18n'; import { getQuestions } from './prompts'; -import { newSystemChoiceValue } from './prompts/datasources/sap-system/new-system/questions'; +import { newSystemChoiceValue } from './prompts/datasources/sap-system/system-selection'; import LoggerHelper from './prompts/logger-helper'; import { DatasourceType, @@ -86,18 +86,24 @@ async function prompt( } export { + // @derecated - temp export to support to support open source migration DatasourceType, + // @deprecated - temp export to support to support open source migration ERROR_TYPE, + // @deprecated - temp export to support to support open source migration ErrorHandler, + // @deprecated - temp export to support to support open source migration OdataVersion, - getPrompts, + // @deprecated - temp export to support to support open source migration newSystemChoiceValue, + // @deprecated - temp export to support to support open source migration + type SapSystemType, + getPrompts, prompt, promptNames, type CapRuntime, type CapService, type InquirerAdapter, type OdataServiceAnswers, - type OdataServicePromptOptions, - type SapSystemType + type OdataServicePromptOptions }; diff --git a/packages/odata-service-inquirer/src/prompts/connectionValidator.ts b/packages/odata-service-inquirer/src/prompts/connectionValidator.ts index edd44d9c1a..78b46a07d5 100644 --- a/packages/odata-service-inquirer/src/prompts/connectionValidator.ts +++ b/packages/odata-service-inquirer/src/prompts/connectionValidator.ts @@ -14,9 +14,17 @@ import { ODataVersion, create, createForAbap, - createForAbapOnCloud + createForAbapOnCloud, + createForDestination } from '@sap-ux/axios-extension'; -import { isAppStudio } from '@sap-ux/btp-utils'; +import { + Authentication, + type Destination, + getDestinationUrlForAppStudio, + isAppStudio, + isFullUrlDestination, + isPartialUrlDestination +} from '@sap-ux/btp-utils'; import https from 'https'; import { ERROR_TYPE, ErrorHandler } from '../error-handler/error-handler'; import { t } from '../i18n'; @@ -40,7 +48,7 @@ interface Validity { canSkipCertError?: boolean; } -type ValidationResult = string | boolean | IValidationLink; +export type ValidationResult = string | boolean | IValidationLink; // Cert errors that may be ignored by prompt user const ignorableCertErrors = [ERROR_TYPE.CERT_SELF_SIGNED, ERROR_TYPE.CERT_SELF_SIGNED_CERT_IN_CHAIN]; @@ -61,8 +69,10 @@ export type SystemAuthType = 'serviceKey' | 'reentranceTicket' | 'basic' | 'unkn */ export class ConnectionValidator { public readonly validity: Validity = {}; - // The current valid url (not necessarily authenticated but the url is in a valid format) + // The current valid url (not necessarily authenticated but the url is in a valid format), for destination connections this will be in the form: ://.dest private _validatedUrl: string | undefined; + // Only in the case of destination connections does this store the destination `url` value + private _destinationUrl: string | undefined; // The current client code used for requests, the client code has been validated by a successful request private _validatedClient: string | undefined; @@ -75,6 +85,7 @@ export class ConnectionValidator { private _serviceInfo: ServiceInfo | undefined; private _connectedUserName: string | undefined; private _connectedSystemName: string | undefined; + // todo: private _isS4HanaCloud: boolean | undefined; private _refreshToken: string | undefined; /** @@ -186,6 +197,15 @@ export class ConnectionValidator { return this._refreshToken; } + /** + * Get the full destination url as defined in the destination configuration. This should not be used to connect from App Studio. + * + * @returns the connected destination 'URL' attribute value + */ + public get destinationUrl(): string | undefined { + return this._destinationUrl; + } + /** * Get the connected system name. If previously set this will be used, otherwise the name is determined * by the system auth type, or the validated url. @@ -321,7 +341,7 @@ export class ConnectionValidator { private async createOdataServiceConnection( axiosConfig: AxiosExtensionRequestConfig & ProviderConfiguration, servicePath: string - ) { + ): Promise { this._axiosConfig = axiosConfig; this._serviceProvider = create(this._axiosConfig); this._odataService = this._serviceProvider.service(servicePath); @@ -348,22 +368,32 @@ export class ConnectionValidator { * @param connectConfig.url the system url * @param connectConfig.serviceInfo the service info * @param connectConfig.odataVersion the odata version to restrict the catalog requests if only a specific version is required + * @param connectConfig.destination */ private async createSystemConnection({ axiosConfig, url, serviceInfo, + destination, odataVersion }: { axiosConfig?: AxiosExtensionRequestConfig & ProviderConfiguration; url?: URL; serviceInfo?: ServiceInfo; + destination?: Destination; odataVersion?: ODataVersion; }): Promise { + // todo: Would it be better to return a boolean or string, calling getValidationResultFromStatusCode() and returning the result? this.resetConnectionState(); + this.resetValidity(); if (this.systemAuthType === 'reentranceTicket' || this.systemAuthType === 'serviceKey') { this._serviceProvider = this.getAbapOnCloudServiceProvider(url, serviceInfo); + } else if (destination) { + // Assumption: the destination configured URL is a valid URL, will be needed later for basic auth error handling + this._validatedUrl = getDestinationUrlForAppStudio(destination.Name); + this._destinationUrl = destination.Host; + this._serviceProvider = createForDestination({}, destination); } else if (axiosConfig) { this._axiosConfig = axiosConfig; this._serviceProvider = createForAbap(axiosConfig); @@ -371,6 +401,7 @@ export class ConnectionValidator { if (this._serviceProvider) { LoggerHelper.attachAxiosLogger(this._serviceProvider.interceptors); + //this._isS4HanaCloud = await (this._serviceProvider as AbapServiceProvider).isS4Cloud(); } if (!odataVersion || odataVersion === ODataVersion.v2) { @@ -433,7 +464,7 @@ export class ConnectionValidator { * @param serviceInfo the service info * @returns the service provider */ - private getAbapOnCloudServiceProvider(url?: URL, serviceInfo?: ServiceInfo): ServiceProvider { + private getAbapOnCloudServiceProvider(url?: URL, serviceInfo?: ServiceInfo): AbapServiceProvider { if (this.systemAuthType === 'reentranceTicket' && url) { return createForAbapOnCloud({ environment: AbapCloudEnvironment.EmbeddedSteampunk, @@ -454,10 +485,12 @@ export class ConnectionValidator { /** * Validate the system connectivity with the specified service info (containing UAA details). + * This will create a connection to the system, updating the service provider reference. + * The connected user name will be cached for later use. * * @param serviceInfo the service info containing the UAA details * @param odataVersion the odata version to restrict the catalog requests if only a specific version is required - * @returns true if the system is reachable, false if not, or an error message string + * @returns true if the system is reachable and authenticated, if required, false if not, or an error message string */ public async validateServiceInfo(serviceInfo: ServiceInfo, odataVersion?: ODataVersion): Promise { if (!serviceInfo) { @@ -480,6 +513,56 @@ export class ConnectionValidator { } } + /** + * Validate the specified destination connectivity. + * + * @param destination the destination to validate + * @param odataVersion the odata version to restrict the catalog requests if only a specific version is required + * @param servicePath the service path to validate, if specified will be appended to the destination URL for validation, if not specified the destination url will be used + * @returns @returns true if the system is reachable and authenticated, if required, false if not, or an error message string + */ + public async validateDestination( + destination: Destination, + odataVersion?: ODataVersion, + servicePath?: string + ): Promise<{ valResult: ValidationResult; errorType?: ERROR_TYPE }> { + try { + // The only supported authentication mechanism for destinations set to Authentication 'NO_AUTHENTICATION' is basic (i.e. to the target Abap system) + // So while we actually dont know we assume its basic for now since thats the only supported mechanism + this.systemAuthType = destination.Authentication === Authentication.NO_AUTHENTICATION ? 'basic' : 'unknown'; + // Since a destination may be a system or a service connection, we need to determine the connection request (catalog or service) + if (isFullUrlDestination(destination) || isPartialUrlDestination(destination)) { + this.resetConnectionState(); + this.resetValidity(); + // Get the destination URL in the BAS specific form ://.dest + const destUrl = getDestinationUrlForAppStudio(destination.Name, servicePath); + // Get the destination URL in the portable form ://: + this._destinationUrl = servicePath + ? new URL(servicePath, destination.Host).toString() + : destination.Host; + const authRequired = await this.isAuthRequired(destUrl, destination['sap-client']); + return { + valResult: authRequired ? ErrorHandler.getErrorMsgFromType(ERROR_TYPE.AUTH) ?? false : true, + errorType: authRequired ? ERROR_TYPE.AUTH : undefined + }; + } else { + await this.createSystemConnection({ destination, odataVersion }); + } + return { + valResult: this.getValidationResultFromStatusCode(200) + }; + } catch (error) { + //LoggerHelper.logger.debug(`ConnectionValidator.validateDestination() - error: ${error.message}`); + if (error?.isAxiosError) { + this.getValidationResultFromStatusCode(error?.response?.status || error?.code); + } + return { + valResult: errorHandler.logErrorMsgs(error) ?? false, + errorType: errorHandler.getCurrentErrorType() ?? ERROR_TYPE.CONNECTION + }; + } + } + /** * Validates the system or service url format as well as its reachability. * @@ -490,6 +573,7 @@ export class ConnectionValidator { * @param options.forceReValidation force re-validation of the url * @param options.isSystem if true, the url will be treated as a system url rather than a service url, this value is retained for subsequent calls * @param options.odataVersion if specified will restrict catalog requests to only the specified odata version + * @param options.systemAuthType the system auth type used to create system connections, if not specified or `isSystem` is false or undefined, `basic` is assumed * @returns true if the url is reachable, false if not, or an error message string */ public async validateUrl( @@ -498,18 +582,24 @@ export class ConnectionValidator { ignoreCertError = false, forceReValidation = false, isSystem = false, - odataVersion + odataVersion, + systemAuthType }: { ignoreCertError?: boolean; forceReValidation?: boolean; isSystem?: boolean; odataVersion?: ODataVersion; + systemAuthType?: SystemAuthType; } = {} ): Promise { if (this.isEmptyString(serviceUrl)) { this.resetValidity(); + this.validity.urlFormat = false; return false; } + if (systemAuthType) { + this.systemAuthType = systemAuthType; + } try { const url = new URL(serviceUrl); if (!forceReValidation && this.isUrlValidated(serviceUrl)) { @@ -570,6 +660,9 @@ export class ConnectionValidator { } else if (ErrorHandler.getErrorType(status) === ERROR_TYPE.CONNECTION) { this.validity.reachable = false; return ErrorHandler.getErrorMsgFromType(ERROR_TYPE.CONNECTION, `http code: ${status}`) ?? false; + } else if (ErrorHandler.getErrorType(status) === ERROR_TYPE.BAD_REQUEST) { + this.validity.reachable = true; + return ErrorHandler.getErrorMsgFromType(ERROR_TYPE.BAD_REQUEST, `http code: ${status}`) ?? false; } this.validity.reachable = true; return true; @@ -601,7 +694,7 @@ export class ConnectionValidator { /** * Check whether basic auth is required for the given url, or for the previously validated url if none specified. - * This will also set the validity state for the url. This will not validate the URL. + * This will also set the validity state for the url. * * @param urlString the url to validate, if not provided the previously validated url will be used * @param client optional, sap client code, if not provided the previously validated client will be used @@ -617,14 +710,17 @@ export class ConnectionValidator { return false; } - // Dont re-request if already validated - if ( - this._validatedUrl === urlString && - this._validatedClient === client && - this.validity.authRequired !== undefined - ) { - return this.validity.authRequired; + // Dont re-request if we have already determined the auth requirement or we are authenticated + if (this._validatedUrl === urlString && this._validatedClient === client) { + if (this.validity.authenticated) { + return false; + } + if (this.validity.authRequired !== undefined) { + return this.validity.authRequired; + } + // Not determined yet, continue } + // New URL or client so we need to re-request try { const url = new URL(urlString); @@ -641,7 +737,9 @@ export class ConnectionValidator { this.validity.authRequired = true; this.validity.reachable = true; } - // Returning undefined if we cannot determine if auth is required + // Since an exception was not thrown, this is a valid url (todo: retest all flows that use this since these 2 loc were added) + this.validity.urlFormat = true; + this._validatedUrl = urlString; return this.validity.authRequired; } catch (error) { errorHandler.logErrorMsgs(error); @@ -697,6 +795,7 @@ export class ConnectionValidator { // Since an exception was not thrown, this is a valid url this.validity.urlFormat = true; this._validatedUrl = url; + const valResult = this.getValidationResultFromStatusCode(status); if (valResult === true) { @@ -708,6 +807,7 @@ export class ConnectionValidator { } return valResult; } catch (error) { + this.resetValidity(); return errorHandler.getErrorMsg(error) ?? false; } } @@ -716,12 +816,13 @@ export class ConnectionValidator { * Reset the validity state. */ private resetValidity(): void { - this.validity.urlFormat = false; + delete this.validity.urlFormat; delete this.validity.reachable; delete this.validity.authRequired; delete this.validity.authenticated; delete this.validity.canSkipCertError; this._validatedUrl = undefined; + this._destinationUrl = undefined; } /** diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-btp/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-btp/questions.ts index 22327074be..3e82ea7560 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-btp/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-btp/questions.ts @@ -11,10 +11,11 @@ import { PromptState, getDefaultChoiceIndex, getHostEnvironment } from '../../.. import { ConnectionValidator } from '../../../connectionValidator'; import LoggerHelper from '../../../logger-helper'; import { errorHandler } from '../../../prompt-helpers'; -import { getSystemServiceQuestion, getSystemUrlQuestion, getUserSystemNameQuestion } from '../new-system/questions'; +import { getSystemUrlQuestion, getUserSystemNameQuestion } from '../new-system/questions'; import { newSystemPromptNames } from '../new-system/types'; import { validateServiceKey } from '../validators'; import { getABAPInstanceChoices } from './cf-helper'; +import { type ServiceAnswer, getSystemServiceQuestion } from '../service-selection'; const abapOnBtpPromptNamespace = 'abapOnBtp'; const systemUrlPromptName = `${abapOnBtpPromptNamespace}:${newSystemPromptNames.newSystemUrl}` as const; @@ -41,10 +42,12 @@ interface AbapOnBtpAnswers extends Partial { * @param promptOptions The prompt options which control the service selection and system name] * @returns The list of questions for the ABAP on BTP system */ -export function getAbapOnBTPSystemQuestions(promptOptions?: OdataServicePromptOptions): Question[] { +export function getAbapOnBTPSystemQuestions( + promptOptions?: OdataServicePromptOptions +): Question[] { PromptState.reset(); const connectValidator = new ConnectionValidator(); - const questions: Question[] = []; + const questions: Question[] = []; questions.push({ type: 'list', name: abapOnBtpPromptNames.abapOnBtpAuthType, @@ -117,6 +120,7 @@ export function getAbapOnBTPSystemQuestions(promptOptions?: OdataServicePromptOp /** * Validate the service info for the ABAP on BTP system. This function will validate the service key file and the connection to the ABAP system. + * Updates the prompt state with the connected system. * * @param abapService the abap service as provided by CF tools * @param connectionValidator connection validator instance diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-prem/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-prem/questions.ts index 5daa1eb111..2c02365ab4 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-prem/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/abap-on-prem/questions.ts @@ -1,34 +1,37 @@ import { withCondition } from '@sap-ux/inquirer-common'; import type { OdataVersion } from '@sap-ux/odata-service-writer'; import { validateClient } from '@sap-ux/project-input-validator'; -import type { Answers, InputQuestion, PasswordQuestion, Question } from 'inquirer'; +import type { InputQuestion, Question } from 'inquirer'; import { t } from '../../../../i18n'; -import type { - OdataServiceAnswers, - OdataServicePromptOptions, - ServiceSelectionPromptOptions, - SystemNamePromptOptions, - promptNames +import { + type OdataServiceAnswers, + type OdataServicePromptOptions, + type ServiceSelectionPromptOptions, + type SystemNamePromptOptions } from '../../../../types'; import { PromptState } from '../../../../utils'; import { ConnectionValidator } from '../../../connectionValidator'; -import { getSystemServiceQuestion, getSystemUrlQuestion, getUserSystemNameQuestion } from '../new-system/questions'; -import { newSystemPromptNames, type ServiceAnswer } from '../new-system/types'; +import { BasicCredentialsPromptNames, getCredentialsPrompts } from '../credentials/questions'; +import { getSystemUrlQuestion, getUserSystemNameQuestion } from '../new-system/questions'; +import { newSystemPromptNames } from '../new-system/types'; +import { getSystemServiceQuestion, type ServiceAnswer } from '../service-selection'; const abapOnPremPromptNamespace = 'abapOnPrem'; const systemUrlPromptName = `${abapOnPremPromptNamespace}:${newSystemPromptNames.newSystemUrl}` as const; +const usernamePromptName = `${abapOnPremPromptNamespace}:${BasicCredentialsPromptNames.systemUsername}` as const; +const passwordPromptName = `${abapOnPremPromptNamespace}:${BasicCredentialsPromptNames.systemPassword}` as const; -export enum abapOnPremInternalPromptNames { - sapClient = 'sapClient', - systemUsername = 'abapSystemUsername', - systemPassword = 'abapSystemPassword' +export enum abapOnPremPromptNames { + sapClient = 'sapClient' } -interface AbapOnPremAnswers extends Partial { +interface AbabpOnPremCredentialsAnswers { + [usernamePromptName]?: string; + [passwordPromptName]?: string; +} + +export interface AbapOnPremAnswers extends Partial, AbabpOnPremCredentialsAnswers { [systemUrlPromptName]?: string; - [abapOnPremInternalPromptNames.systemUsername]?: string; - [abapOnPremInternalPromptNames.systemPassword]?: string; - [promptNames.serviceSelection]?: ServiceAnswer; } /** @@ -37,13 +40,15 @@ interface AbapOnPremAnswers extends Partial { * @param promptOptions options for prompts. Applicable options are: {@link ServiceSelectionPromptOptions}, {@link SystemNamePromptOptions} * @returns property questions for the Abap on-premise datasource */ -export function getAbapOnPremQuestions(promptOptions?: OdataServicePromptOptions): Question[] { +export function getAbapOnPremQuestions( + promptOptions?: OdataServicePromptOptions +): Question[] { PromptState.reset(); const connectValidator = new ConnectionValidator(); // Prompt options const requiredOdataVersion = promptOptions?.serviceSelection?.requiredOdataVersion; - const questions: Question[] = getAbapOnPremSystemQuestions( + const questions: Question[] = getAbapOnPremSystemQuestions( promptOptions?.userSystemName, connectValidator, requiredOdataVersion @@ -68,13 +73,15 @@ export function getAbapOnPremSystemQuestions( requiredOdataVersion?: OdataVersion ): Question[] { const connectValidator = connectionValidator ?? new ConnectionValidator(); - let validClient = true; + // Object reference to access dynamic sapClient value in prompts where the previous answers are not available. + // This allows re-usability of the credentials prompts where a client prompt was not used (client was loaded from store). + const sapClientRef: { sapClient: string | undefined; isValid: boolean } = { sapClient: undefined, isValid: true }; const questions: Question[] = [ getSystemUrlQuestion(connectValidator, abapOnPremPromptNamespace, requiredOdataVersion), { type: 'input', - name: abapOnPremInternalPromptNames.sapClient, + name: abapOnPremPromptNames.sapClient, message: t('prompts.sapClient.message'), guiOptions: { breadcrumb: t('prompts.sapClient.breadcrumb') @@ -82,57 +89,20 @@ export function getAbapOnPremSystemQuestions( validate: (client) => { const valRes = validateClient(client); if (valRes === true) { - validClient = true; + sapClientRef.sapClient = client; + sapClientRef.isValid = true; return true; } - validClient = false; + sapClientRef.sapClient = undefined; + sapClientRef.isValid = false; return valRes; } } as InputQuestion, - { - when: () => connectValidator.isAuthRequired(), - type: 'input', - name: abapOnPremInternalPromptNames.systemUsername, - message: t('prompts.systemUsername.message'), - guiOptions: { - mandatory: true - }, - default: '', - validate: (user: string) => user?.length > 0 - } as InputQuestion, - { - when: () => connectValidator.isAuthRequired(), - type: 'password', - guiOptions: { - mandatory: true - }, - name: abapOnPremInternalPromptNames.systemPassword, - message: t('prompts.systemPassword.message'), - guiType: 'login', - mask: '*', - default: '', - validate: async (password, answers: AbapOnPremAnswers & Answers) => { - if (!(connectValidator.validatedUrl && answers.abapSystemUsername && password && validClient)) { - return false; - } - const valResult = await connectValidator.validateAuth( - connectValidator.validatedUrl, - answers.abapSystemUsername, - password, - { - sapClient: answers.sapClient, - isSystem: true - } - ); - if (valResult === true && connectValidator.serviceProvider) { - PromptState.odataService.connectedSystem = { - serviceProvider: connectValidator.serviceProvider - }; - return true; - } - return valResult; - } - } as PasswordQuestion + ...getCredentialsPrompts( + connectValidator, + abapOnPremPromptNamespace, + sapClientRef + ) ]; if (systemNamePromptOptions?.hide !== true) { diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/credentials/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/credentials/questions.ts new file mode 100644 index 0000000000..226b9ea49e --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/credentials/questions.ts @@ -0,0 +1,128 @@ +import type { BackendSystem } from '@sap-ux/store'; +import type { Answers, InputQuestion, PasswordQuestion, Question } from 'inquirer'; +import { t } from '../../../../i18n'; +import { promptNames } from '../../../../types'; +import { PromptState } from '../../../../utils'; +import type { ConnectionValidator } from '../../../connectionValidator'; +import type { SystemSelectionAnswerType } from '../system-selection'; +import { isFullUrlDestination, isPartialUrlDestination, type Destination } from '@sap-ux/btp-utils'; + +export enum BasicCredentialsPromptNames { + systemUsername = 'systemUsername', + systemPassword = 'systemPassword' +} +/** + * Re-usable credentials prompts for connection to systems using basic auth. + * + * @param connectionValidator + * @param promptNamespace + * @param sapClient + * @param sapClient.sapClient the sapClient value to be used along with the credentials validation + * @param sapClient.isValid validation of credentials is deferred until a valid sapClient is provided or undefined + * @returns the credentials prompts + */ +export function getCredentialsPrompts( + connectionValidator: ConnectionValidator, + promptNamespace?: string, + sapClient?: { sapClient: string | undefined; isValid: boolean } +): Question[] { + const usernamePromptName = `${promptNamespace ? promptNamespace + ':' : ''}${ + BasicCredentialsPromptNames.systemUsername + }`; + const passwordPromptName = `${promptNamespace ? promptNamespace + ':' : ''}${ + BasicCredentialsPromptNames.systemPassword + }`; + + // Optimization to prevent re-checking of auth + let authRequired: boolean | undefined; + + return [ + { + when: async () => { + authRequired = await connectionValidator.isAuthRequired(); + return connectionValidator.systemAuthType === 'basic' && authRequired; + }, + type: 'input', + name: usernamePromptName, + message: t('prompts.systemUsername.message'), + guiOptions: { + mandatory: true + }, + default: '', + validate: (user: string) => user?.length > 0 + } as InputQuestion, + { + when: () => connectionValidator.systemAuthType === 'basic' && authRequired, + type: 'password', + guiOptions: { + mandatory: true + }, + guiType: 'login', + name: passwordPromptName, + message: t('prompts.systemPassword.message'), + mask: '*', + default: '', + validate: async (password, answers: T) => { + if ( + !( + connectionValidator.validatedUrl && + answers?.[usernamePromptName] && + password && + (sapClient?.isValid || !sapClient) + ) + ) { + return false; + } + // We may have a previously selected system + const selectedSytem = answers?.[promptNames.systemSelection] as SystemSelectionAnswerType; + let selectedSystemClient; + let isSystem = true; + if (selectedSytem?.type === 'backendSystem') { + selectedSystemClient = (selectedSytem.system as BackendSystem)?.client; + } else if (selectedSytem?.type === 'destination') { + const destination = selectedSytem.system as Destination; + selectedSystemClient = destination['sap-client']; + if (isFullUrlDestination(destination) || isPartialUrlDestination(destination)) { + isSystem = false; + } + } + + const valResult = await connectionValidator.validateAuth( + connectionValidator.validatedUrl, + answers?.[usernamePromptName], + password, + { + sapClient: sapClient?.sapClient || selectedSystemClient, + isSystem + } + ); + if (valResult === true && connectionValidator.serviceProvider) { + PromptState.odataService.connectedSystem = { + serviceProvider: connectionValidator.serviceProvider + }; + // If the connection is successful and an existing backend system was selected, + // update the existing backend system with the new credentials that may be used to update in the store. + if (selectedSytem?.type === 'backendSystem') { + const backendSystem = selectedSytem.system as BackendSystem; + // Have the credentials changed.. + if ( + backendSystem.username !== answers?.[usernamePromptName] || + backendSystem.password !== password + ) { + PromptState.odataService.connectedSystem.backendSystem = Object.assign(backendSystem, { + username: answers?.[usernamePromptName], + password, + newOrUpdated: true + } as Partial); + } + // If the connection is successful and a destination was selected, assign the connected destination to the prompt state. + } else if (selectedSytem?.type === 'destination') { + PromptState.odataService.connectedSystem.destination = selectedSytem.system as Destination; + } + return true; + } + return valResult; + } + } as PasswordQuestion + ]; +} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/questions.ts index 2917a46d20..b174952a3b 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/questions.ts @@ -1,33 +1,21 @@ /** * New system prompting questions for re-use in multiple sap-system datasource prompt sets. */ -import type { IMessageSeverity } from '@sap-devx/yeoman-ui-types'; -import { Severity } from '@sap-devx/yeoman-ui-types'; -import type { CatalogService, V2CatalogService } from '@sap-ux/axios-extension'; -import { ODataVersion, ServiceType } from '@sap-ux/axios-extension'; -import { searchChoices, withCondition, type ListQuestion, type InputQuestion } from '@sap-ux/inquirer-common'; +import { withCondition, type InputQuestion, type ListQuestion } from '@sap-ux/inquirer-common'; import type { OdataVersion } from '@sap-ux/odata-service-writer'; import { AuthenticationType, BackendSystem } from '@sap-ux/store'; -import type { Answers, ListChoiceOptions, Question } from 'inquirer'; +import type { Answers, Question } from 'inquirer'; import { t } from '../../../../i18n'; -import type { OdataServiceAnswers, OdataServicePromptOptions, SapSystemType, ValidationLink } from '../../../../types'; -import { hostEnvironment, promptNames } from '../../../../types'; -import { PromptState, convertODataVersionType, getDefaultChoiceIndex, getHostEnvironment } from '../../../../utils'; +import type { OdataServicePromptOptions, SapSystemType } from '../../../../types'; +import { promptNames } from '../../../../types'; +import { PromptState, convertODataVersionType } from '../../../../utils'; import type { ConnectionValidator, SystemAuthType } from '../../../connectionValidator'; +import { getAbapOnBTPSystemQuestions } from '../abap-on-btp/questions'; +import { getAbapOnPremQuestions } from '../abap-on-prem/questions'; import { suggestSystemName } from '../prompt-helpers'; import { validateSystemName } from '../validators'; -import { getServiceChoices, getServiceDetails, getServiceType } from './service-helper'; -import { type ServiceAnswer, newSystemPromptNames } from './types'; -import { getAbapOnPremQuestions } from '../abap-on-prem/questions'; -import { getAbapOnBTPSystemQuestions } from '../abap-on-btp/questions'; -import LoggerHelper from '../../../logger-helper'; -import { errorHandler } from '../../../prompt-helpers'; -import { ERROR_TYPE, ErrorHandler } from '../../../../error-handler/error-handler'; +import { newSystemPromptNames } from './types'; -// New system choice value is a hard to guess string to avoid conflicts with existing system names or user named systems -// since it will be used as a new system value in the system selection prompt. -export const newSystemChoiceValue = '!@£*&937newSystem*X~qy^'; -const cliServicePromptName = 'cliServiceSelection'; /** * Internal only answers to service URL prompting not returned with OdataServiceAnswers. */ @@ -36,14 +24,6 @@ export interface NewSystemAnswers { [promptNames.userSystemName]?: string; } -const systemSelectionPromptNames = { - system: 'system' -} as const; - -export interface SystemSelectionAnswer extends OdataServiceAnswers { - [systemSelectionPromptNames.system]?: string; -} - /** * Convert the system connection scheme (Service Key, Rentrance Ticket, etc) to the store specific authentication type. * Note the absence of CF Discovery, in this case the service key file (UAA) is also used for the Abap connectivity. @@ -127,11 +107,15 @@ export function getSystemUrlQuestion( isSystem: true, odataVersion: convertODataVersionType(requiredOdataVersion) }); - // If basic auth not required we should have an active connection - if (valResult === true && !connectValidator.validity.authRequired && connectValidator.serviceProvider) { - PromptState.odataService.connectedSystem = { - serviceProvider: connectValidator.serviceProvider - }; + // If basic auth not required we should have an active connection (could be a re-entrance ticket supported system url) + if (valResult === true) { + if (!connectValidator.validity.authRequired && connectValidator.serviceProvider) { + PromptState.odataService.connectedSystem = { + serviceProvider: connectValidator.serviceProvider + }; + } else { + connectValidator.systemAuthType = 'basic'; + } } return valResult; } @@ -205,6 +189,7 @@ export function getUserSystemNameQuestion( refreshToken: connectValidator.refreshToken }); PromptState.odataService.connectedSystem.backendSystem = backendSystem; + PromptState.odataService.connectedSystem.backendSystem.newOrUpdated = true; } } return isValid; @@ -213,166 +198,3 @@ export function getUserSystemNameQuestion( return newSystemNamePrompt; } - -/** - * Create a value for the service selection prompt message, which may include thge active connected user name. - * - * @param username The connected user name - * @returns The service selection prompt message - */ -export function getSelectedServiceLabel(username: string | undefined): string { - let message = t('prompts.systemService.message'); - if (username) { - message = message.concat(` ${t('texts.forUserName', { username })}`); - } - return message; -} - -/** - * Get the service selection prompt for a system connection. The service selection prompt is used to select a service from the system catalog. - * - * @param connectValidator A reference to the active connection validator, used to validate the service selection and retrieve service details. - * @param promptNamespace The namespace for the prompt, used to identify the prompt instance and namespaced answers. - * This is used to avoid conflicts with other prompts of the same types. - * @param promptOptions Options for the service selection prompt see {@link OdataServicePromptOptions} - * @returns the service selection prompt - */ -export function getSystemServiceQuestion( - connectValidator: ConnectionValidator, - promptNamespace: string, - promptOptions?: OdataServicePromptOptions -): Question[] { - let serviceChoices: ListChoiceOptions[] = []; - // Prevent re-requesting services repeatedly by only requesting them once and when the system or client is changed - let previousSystemUrl: string | undefined; - let previousClient: string | undefined; - let previousService: ServiceAnswer | undefined; - const requiredOdataVersion = promptOptions?.serviceSelection?.requiredOdataVersion; - - const newSystemServiceQuestion = { - when: (): boolean => - connectValidator.validity.authenticated || connectValidator.validity.authRequired === false, - name: `${promptNamespace}:${promptNames.serviceSelection}`, - type: promptOptions?.serviceSelection?.useAutoComplete ? 'autocomplete' : 'list', - message: () => getSelectedServiceLabel(connectValidator.connectedUserName), - guiOptions: { - breadcrumb: t('prompts.systemService.breadcrumb'), - mandatory: true, - applyDefaultWhenDirty: true - }, - source: (prevAnswers: T, input: string) => searchChoices(input, serviceChoices as ListChoiceOptions[]), - choices: async () => { - if ( - serviceChoices.length === 0 || - previousSystemUrl !== connectValidator.validatedUrl || - previousClient !== connectValidator.validatedClient - ) { - let catalogs: CatalogService[] = []; - if (requiredOdataVersion && connectValidator.catalogs[requiredOdataVersion]) { - catalogs.push(connectValidator.catalogs[requiredOdataVersion]!); - } else { - catalogs = Object.values(connectValidator.catalogs).filter( - (cat) => cat !== undefined - ) as CatalogService[]; - } - previousSystemUrl = connectValidator.validatedUrl; - previousClient = connectValidator.validatedClient; - serviceChoices = await getServiceChoices(catalogs); - } - return serviceChoices; - }, - additionalMessages: (selectedService: ServiceAnswer) => - getSelectedServiceMessage(serviceChoices, selectedService, connectValidator, requiredOdataVersion), - default: () => getDefaultChoiceIndex(serviceChoices as Answers[]), - // Warning: only executes in YUI not cli - validate: async (service: ServiceAnswer): Promise => { - if (!connectValidator.validatedUrl) { - return false; - } - // if no choices are available and an error is present, return the error message - if (serviceChoices.length === 0 && errorHandler.hasError()) { - return ErrorHandler.getHelpForError(ERROR_TYPE.SERVICES_UNAVAILABLE) ?? false; - } - // Dont re-request the same service details - if (service && previousService?.servicePath !== service.servicePath) { - previousService = service; - return getServiceDetails(service, connectValidator); - } - return true; - } - } as ListQuestion; - - const questions: Question[] = [newSystemServiceQuestion]; - - // Only for CLI use as `list` prompt validation does not run on CLI - if (getHostEnvironment() === hostEnvironment.cli) { - questions.push({ - when: async (answers: Answers): Promise => { - const selectedService = answers?.[`${promptNamespace}:${promptNames.serviceSelection}`]; - if (selectedService && connectValidator.validatedUrl) { - const result = await getServiceDetails(selectedService, connectValidator); - if (typeof result === 'string') { - LoggerHelper.logger.error(result); - throw new Error(result); - } - } - if (serviceChoices.length === 0 && errorHandler.hasError()) { - const noServicesError = ErrorHandler.getHelpForError(ERROR_TYPE.SERVICES_UNAVAILABLE)!.toString(); - throw new Error(noServicesError); - } - return false; - }, - name: `${promptNamespace}:${cliServicePromptName}` - } as Question); - } - return questions; -} - -/** - * Get the service selection prompt additional message. This prompt will make an additional call to the system backend - * to retrieve the service type and display a warning message if the service type is not UI. - * - * @param serviceChoices a list of service choices - * @param selectedService the selected service - * @param connectValidator the connection validator - * @param requiredOdataVersion the required OData version for the service - * @returns the service selection prompt additional message - */ -async function getSelectedServiceMessage( - serviceChoices: ListChoiceOptions[], - selectedService: ServiceAnswer, - connectValidator: ConnectionValidator, - requiredOdataVersion?: OdataVersion -): Promise { - if (serviceChoices?.length === 0) { - if (requiredOdataVersion) { - return { - message: t('prompts.warnings.noServicesAvailableForOdataVersion', { - odataVersion: requiredOdataVersion - }), - severity: Severity.warning - }; - } else { - return { - message: t('prompts.warnings.noServicesAvailable'), - severity: Severity.warning - }; - } - } - if (selectedService) { - let serviceType = selectedService.serviceType; - if (selectedService.serviceODataVersion === ODataVersion.v2) { - serviceType = await getServiceType( - selectedService.servicePath, - selectedService.serviceType, - connectValidator.catalogs[ODataVersion.v2] as V2CatalogService - ); - } - if (serviceType && serviceType !== ServiceType.UI) { - return { - message: t('prompts.warnings.nonUIServiceTypeWarningMessage', { serviceType: 'A2X' }), - severity: Severity.warning - }; - } - } -} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/types.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/types.ts index c0be302d47..ab278728d8 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/types.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/types.ts @@ -1,15 +1,3 @@ -import type { ODataVersion } from '@sap-ux/axios-extension'; - -/** - * Sap System service answer - */ -export type ServiceAnswer = { - servicePath: string; - serviceODataVersion: ODataVersion; - toString: () => string; - serviceType?: string; -}; - export const newSystemPromptNames = { newSystemType: 'newSystemType', newSystemUrl: 'newSystemUrl', diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/index.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/index.ts new file mode 100644 index 0000000000..c9b7e9bd0e --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/index.ts @@ -0,0 +1,2 @@ +export * from './questions'; +export * from './types'; diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/questions.ts new file mode 100644 index 0000000000..774c7a802b --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/questions.ts @@ -0,0 +1,142 @@ +/** + * Service selection prompting for SAP systems. Used by new and existing system prompts. + * + */ +import type { CatalogService } from '@sap-ux/axios-extension'; +import { searchChoices, type ListQuestion } from '@sap-ux/inquirer-common'; +import type { Answers, ListChoiceOptions, Question } from 'inquirer'; +import { ERROR_TYPE, ErrorHandler } from '../../../../error-handler/error-handler'; +import { t } from '../../../../i18n'; +import type { OdataServicePromptOptions, ValidationLink } from '../../../../types'; +import { hostEnvironment, promptNames } from '../../../../types'; +import { getDefaultChoiceIndex, getHostEnvironment } from '../../../../utils'; +import type { ConnectionValidator } from '../../../connectionValidator'; +import LoggerHelper from '../../../logger-helper'; +import { errorHandler } from '../../../prompt-helpers'; +import { + getSelectedServiceLabel, + getSelectedServiceMessage, + getServiceChoices, + getServiceDetails +} from '../service-selection/service-helper'; +import { type ServiceAnswer } from './types'; +import { OdataVersion } from '@sap-ux/odata-service-writer'; + +const cliServicePromptName = 'cliServiceSelection'; + +/** + * Get the service selection prompt for an Abap system connection (on-prem or on-btp). The service selection prompt is used to select a service from the system catalog. + * + * @param connectValidator A reference to the active connection validator, used to validate the service selection and retrieve service details. + * @param promptNamespace The namespace for the prompt, used to identify the prompt instance and namespaced answers. + * This is used to avoid conflicts with other prompts of the same types. + * @param promptOptions Options for the service selection prompt see {@link OdataServicePromptOptions} + * @returns the service selection prompt + */ +export function getSystemServiceQuestion( + connectValidator: ConnectionValidator, + promptNamespace: string, + promptOptions?: OdataServicePromptOptions +): Question[] { + let serviceChoices: ListChoiceOptions[] = []; + // Prevent re-requesting services repeatedly by only requesting them once and when the system or client is changed + let previousSystemUrl: string | undefined; + let previousClient: string | undefined; + let previousService: ServiceAnswer | undefined; + const requiredOdataVersion = promptOptions?.serviceSelection?.requiredOdataVersion; + + const systemServiceQuestion = { + when: (): boolean => + connectValidator.validity.authenticated || connectValidator.validity.authRequired === false, + name: `${promptNamespace}:${promptNames.serviceSelection}`, + type: promptOptions?.serviceSelection?.useAutoComplete ? 'autocomplete' : 'list', + message: () => getSelectedServiceLabel(connectValidator.connectedUserName), + guiOptions: { + breadcrumb: t('prompts.systemService.breadcrumb'), + mandatory: true, + applyDefaultWhenDirty: true + }, + source: (prevAnswers: unknown, input: string) => searchChoices(input, serviceChoices as ListChoiceOptions[]), + choices: async () => { + if ( + serviceChoices.length === 0 || + previousSystemUrl !== connectValidator.validatedUrl || + previousClient !== connectValidator.validatedClient + ) { + // if we have a catalog, use it to list services + if (connectValidator.catalogs[OdataVersion.v2] || connectValidator.catalogs[OdataVersion.v4]) { + let catalogs: CatalogService[] = []; + if (requiredOdataVersion && connectValidator.catalogs[requiredOdataVersion]) { + catalogs.push(connectValidator.catalogs[requiredOdataVersion]!); + } else { + catalogs = Object.values(connectValidator.catalogs).filter( + (cat) => cat !== undefined + ) as CatalogService[]; + } + previousSystemUrl = connectValidator.validatedUrl; + previousClient = connectValidator.validatedClient; + serviceChoices = await getServiceChoices(catalogs); + } else if (connectValidator.odataService && connectValidator.validatedUrl) { + // We have connected to a service endpoint, use this service as the only choice + const servicePath = new URL(connectValidator.destinationUrl ?? connectValidator.validatedUrl) + .pathname; + serviceChoices = [ + { + name: servicePath, + value: { + servicePath + } as ServiceAnswer + } + ]; + } else { + LoggerHelper.logger.error(t('error.noCatalogOrServiceAvailable')); + } + } + return serviceChoices; + }, + additionalMessages: (selectedService: ServiceAnswer) => + getSelectedServiceMessage(serviceChoices, selectedService, connectValidator, requiredOdataVersion), + default: () => getDefaultChoiceIndex(serviceChoices as Answers[]), + // Warning: only executes in YUI not cli + validate: async (service: ServiceAnswer): Promise => { + if (!connectValidator.validatedUrl) { + return false; + } + // if no choices are available and an error is present, return the error message + if (serviceChoices.length === 0 && errorHandler.hasError()) { + return ErrorHandler.getHelpForError(ERROR_TYPE.SERVICES_UNAVAILABLE) ?? false; + } + // Dont re-request the same service details + if (service && previousService?.servicePath !== service.servicePath) { + previousService = service; + return getServiceDetails(service, connectValidator, requiredOdataVersion); + } + return true; + } + } as ListQuestion; + + const questions: Question[] = [systemServiceQuestion]; + + // Only for CLI use as `list` prompt validation does not run on CLI + if (getHostEnvironment() === hostEnvironment.cli) { + questions.push({ + when: async (answers: Answers): Promise => { + const selectedService = answers?.[`${promptNamespace}:${promptNames.serviceSelection}`]; + if (selectedService && connectValidator.validatedUrl) { + const result = await getServiceDetails(selectedService, connectValidator); + if (typeof result === 'string') { + LoggerHelper.logger.error(result); + throw new Error(result); + } + } + if (serviceChoices.length === 0 && errorHandler.hasError()) { + const noServicesError = ErrorHandler.getHelpForError(ERROR_TYPE.SERVICES_UNAVAILABLE)!.toString(); + throw new Error(noServicesError); + } + return false; + }, + name: `${promptNamespace}:${cliServicePromptName}` + } as Question); + } + return questions; +} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/service-helper.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/service-helper.ts similarity index 60% rename from packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/service-helper.ts rename to packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/service-helper.ts index 9ef6031532..c2dbea706b 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/new-system/service-helper.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/service-helper.ts @@ -1,20 +1,22 @@ +import { type IMessageSeverity, Severity } from '@sap-devx/yeoman-ui-types'; +import type { ODataService } from '@sap-ux/axios-extension'; import { - type CatalogService, - ServiceType, - V2CatalogService, type Annotations, + type CatalogService, type ODataServiceInfo, - type ServiceProvider, - ODataVersion + ODataVersion, + ServiceType, + V2CatalogService } from '@sap-ux/axios-extension'; +import { OdataVersion } from '@sap-ux/odata-service-writer'; import type { ListChoiceOptions } from 'inquirer'; import { t } from '../../../../i18n'; -import LoggerHelper from '../../../logger-helper'; -import type { ServiceAnswer } from './types'; -import type { ConnectionValidator } from '../../../connectionValidator'; import { PromptState } from '../../../../utils'; -import { OdataVersion } from '@sap-ux/odata-service-writer'; +import type { ConnectionValidator } from '../../../connectionValidator'; +import LoggerHelper from '../../../logger-helper'; import { errorHandler } from '../../../prompt-helpers'; +import type { ServiceAnswer } from './types'; +import { validateODataVersion } from '../../../validators'; // Service ids continaining these paths should not be offered as UI compatible services const nonUIServicePaths = ['/IWBEP/COMMON/']; @@ -112,29 +114,29 @@ export async function getServiceChoices(catalogs: CatalogService[]): Promise, string error message or true if successful */ -export async function getServiceMetadata( +async function getServiceMetadata( servicePath: string, - catalog: CatalogService, - serviceProvider: ServiceProvider -): Promise<{ annotations: Annotations[]; metadata: string; serviceProvider: ServiceProvider } | string> { + odataService: ODataService, + catalog?: CatalogService +): Promise<{ annotations: Annotations[]; metadata: string } | string> { let annotations: Annotations[] = []; try { - try { - annotations = await catalog.getAnnotations({ path: servicePath }); - } catch { - LoggerHelper.logger.info(t('prompts.validationMessages.noAnnotations')); + if (catalog) { + try { + annotations = await catalog.getAnnotations({ path: servicePath }); + } catch { + LoggerHelper.logger.info(t('prompts.validationMessages.noAnnotations')); + } } - const odataService = serviceProvider.service(servicePath); const metadata = await odataService.metadata(); return { annotations, - metadata, - serviceProvider + metadata }; } catch (error) { LoggerHelper.logger.error(t('errors.serviceMetadataErrorLog', { servicePath, error })); @@ -172,32 +174,111 @@ export async function getServiceType( * * @param service the specific service to get details for * @param connectionValidator a reference to the connection validator which has an active connection to the backend + * @param requiredOdataVersion * @returns true if successful, setting the PromptState.odataService properties, or an error message indicating why the service details could not be retrieved. */ export async function getServiceDetails( service: ServiceAnswer, - connectionValidator: ConnectionValidator + connectionValidator: ConnectionValidator, + requiredOdataVersion?: OdataVersion ): Promise { - const serviceCatalog = connectionValidator.catalogs[service.serviceODataVersion]; + const serviceCatalog = connectionValidator.catalogs?.[service.serviceODataVersion]; - if (!serviceCatalog || !connectionValidator.serviceProvider) { - LoggerHelper.logger.error('ConenctionValidator is not initialized'); + if (!connectionValidator.serviceProvider) { + LoggerHelper.logger.error('ConnectionValidator connection is not initialized'); return false; } + // We may already have an odata service endpoint connection + let odataService = connectionValidator.odataService; + if (!odataService) { + odataService = connectionValidator.serviceProvider.service(service.servicePath); + } - const serviceResult = await getServiceMetadata( - service.servicePath, - serviceCatalog, - connectionValidator.serviceProvider - ); + const serviceResult = await getServiceMetadata(service.servicePath, odataService, serviceCatalog); if (typeof serviceResult === 'string') { return serviceResult; } + + const { validationMsg, version } = validateODataVersion(serviceResult.metadata, requiredOdataVersion); + if (validationMsg) { + return validationMsg; + } + + // If destinationUrl is available, use it, as validatedUrl may be in the form :.dest + const url = connectionValidator.destinationUrl ?? connectionValidator.validatedUrl; + let origin; + if (url) { + origin = new URL(url).origin; + } PromptState.odataService.annotations = serviceResult?.annotations; PromptState.odataService.metadata = serviceResult?.metadata; PromptState.odataService.odataVersion = - service.serviceODataVersion === ODataVersion.v2 ? OdataVersion.v2 : OdataVersion.v4; + version ?? service.serviceODataVersion === ODataVersion.v2 ? OdataVersion.v2 : OdataVersion.v4; PromptState.odataService.servicePath = service.servicePath; - PromptState.odataService.origin = connectionValidator.validatedUrl; + PromptState.odataService.origin = origin; + PromptState.odataService.sapClient = connectionValidator.validatedClient; return true; } + +/** + * Create a value for the service selection prompt message, which may include thge active connected user name. + * + * @param username The connected user name + * @returns The service selection prompt message + */ +export function getSelectedServiceLabel(username: string | undefined): string { + let message = t('prompts.systemService.message'); + if (username) { + message = message.concat(` ${t('texts.forUserName', { username })}`); + } + return message; +} + +/** + * Get the service selection prompt additional message. This prompt will make an additional call to the system backend + * to retrieve the service type and display a warning message if the service type is not UI. + * + * @param serviceChoices a list of service choices + * @param selectedService the selected service + * @param connectValidator the connection validator + * @param requiredOdataVersion the required OData version for the service + * @returns the service selection prompt additional message + */ +export async function getSelectedServiceMessage( + serviceChoices: ListChoiceOptions[], + selectedService: ServiceAnswer, + connectValidator: ConnectionValidator, + requiredOdataVersion?: OdataVersion +): Promise { + if (serviceChoices?.length === 0) { + if (requiredOdataVersion) { + return { + message: t('prompts.warnings.noServicesAvailableForOdataVersion', { + odataVersion: requiredOdataVersion + }), + severity: Severity.warning + }; + } else { + return { + message: t('prompts.warnings.noServicesAvailable'), + severity: Severity.warning + }; + } + } + if (selectedService) { + let serviceType = selectedService.serviceType; + if (selectedService.serviceODataVersion === ODataVersion.v2) { + serviceType = await getServiceType( + selectedService.servicePath, + selectedService.serviceType, + connectValidator.catalogs[ODataVersion.v2] as V2CatalogService + ); + } + if (serviceType && serviceType !== ServiceType.UI) { + return { + message: t('prompts.warnings.nonUIServiceTypeWarningMessage', { serviceType: 'A2X' }), + severity: Severity.warning + }; + } + } +} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/types.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/types.ts new file mode 100644 index 0000000000..2818022ade --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/service-selection/types.ts @@ -0,0 +1,10 @@ +import type { ODataVersion } from '@sap-ux/axios-extension'; +/** + * Sap System service answer + */ +export type ServiceAnswer = { + servicePath: string; + serviceODataVersion: ODataVersion; + toString: () => string; + serviceType?: string; +}; diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/index.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/index.ts new file mode 100644 index 0000000000..076bf92228 --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/index.ts @@ -0,0 +1 @@ +export * from './questions'; diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/prompt-helpers.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/prompt-helpers.ts new file mode 100644 index 0000000000..7cd199a03d --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/prompt-helpers.ts @@ -0,0 +1,235 @@ +import type { Destination, ServiceInfo } from '@sap-ux/btp-utils'; +import { + isAppStudio, + isFullUrlDestination, + isPartialUrlDestination, + listDestinations, + WebIDEUsage +} from '@sap-ux/btp-utils'; +import type { OdataVersion } from '@sap-ux/odata-service-writer'; +import type { BackendSystem } from '@sap-ux/store'; +import { SystemService } from '@sap-ux/store'; +import type { ListChoiceOptions } from 'inquirer'; +import { ERROR_TYPE } from '../../../../error-handler/error-handler'; +import { t } from '../../../../i18n'; +import { type DestinationFilters, type SapSystemType } from '../../../../types'; +import { convertODataVersionType, PromptState } from '../../../../utils'; +import type { ConnectionValidator, ValidationResult } from '../../../connectionValidator'; +import LoggerHelper from '../../../logger-helper'; +import { newSystemChoiceValue, type SystemSelectionAnswers, type SystemSelectionAnswerType } from './questions'; + +/** + * Connects to the specified backend system and validates the connection. + * Note this will return true in the case of basic auth validation failure to defer validation to the credentials prompt. + * + * @param backendSystem the backend system to connect to + * @param connectionValidator the connection validator to use for the connection + * @param requiredOdataVersion the required OData version for the service, this will be used to narrow the catalog service connections + * @returns the validation result of the backend system connection + */ +export async function connectWithBackendSystem( + backendSystem: BackendSystem, + connectionValidator: ConnectionValidator, + requiredOdataVersion?: OdataVersion +): Promise { + // Create a new connection with the selected system + let connectValResult: ValidationResult = false; + if (!isAppStudio() && backendSystem) { + // Assumption: non-BAS systems are BackendSystems + if (backendSystem.authenticationType === 'reentranceTicket') { + connectValResult = await connectionValidator.validateUrl(backendSystem.url, { + isSystem: true, + odataVersion: convertODataVersionType(requiredOdataVersion), + systemAuthType: 'reentranceTicket' + }); + } else if (backendSystem.serviceKeys) { + connectValResult = await connectionValidator.validateServiceInfo(backendSystem.serviceKeys as ServiceInfo); + } else if ( + (backendSystem.authenticationType === 'basic' || !backendSystem.authenticationType) && + backendSystem.username && + backendSystem.password + ) { + connectValResult = await connectionValidator.validateAuth( + backendSystem.url, + backendSystem.username, + backendSystem.password, + { + isSystem: true, + odataVersion: convertODataVersionType(requiredOdataVersion), + sapClient: backendSystem.client + } + ); + // If authentication failed with existing credentials the user will be prompted to enter new credentials. + // Returning true will effectively defer validation to the credentials prompt. + // We log the error in case there is another issue (unresolveable) with the stored backend configuration. + if (connectValResult !== true) { + LoggerHelper.logger.error( + t('errors.storedSystemConnectionError', { systemName: backendSystem.name, error: connectValResult }) + ); + return true; + } + } + // If the connection is successful, we will return the connected system from the inquirer + if (connectValResult === true && connectionValidator.serviceProvider) { + PromptState.odataService.connectedSystem = { + serviceProvider: connectionValidator.serviceProvider, + backendSystem + }; + } + } + return connectValResult; +} + +/** + * Connects to the specified destination and validates the connection. + * Note that a destination may be a system or a service connection. + * + * @param destination the destination specifying the connection details + * @param connectionValidator the connection validator to use for the connection + * @param requiredOdataVersion the required OData version for the service, this will be used to narrow the catalog service connections + * @param addServicePath the service path to add to the destination URL + * @returns the validation result of the destination connection attempt + */ +export async function connectWithDestination( + destination: Destination, + connectionValidator: ConnectionValidator, + requiredOdataVersion?: OdataVersion, + addServicePath?: string +): Promise { + const { valResult: connectValResult, errorType } = await connectionValidator.validateDestination( + destination, + convertODataVersionType(requiredOdataVersion), + addServicePath + ); + + // If authentication failed with an auth error, and the system connection auth type is basic, we will defer validation to the credentials prompt. + if (errorType === ERROR_TYPE.AUTH && connectionValidator.systemAuthType === 'basic') { + LoggerHelper.logger.error( + t('errors.destinationAuthError', { systemName: destination.Name, error: connectValResult }) + ); + return true; + } + + // If the connection is successful, we will return the connected system from the inquirer + if (connectValResult === true && connectionValidator.serviceProvider) { + PromptState.odataService.connectedSystem = { + serviceProvider: connectionValidator.serviceProvider, + destination + }; + } + + return connectValResult; +} + +/** + * Creates and returns a display name for the system, appending the system type and user display name if available. + * + * @param system the backend system to create a display name for + * @returns the display name for the system + */ +export function getSystemDisplayName(system: BackendSystem): string { + const userDisplayName = system.userDisplayName ? ` [${system.userDisplayName}]` : ''; + const systemTypeName = + system.authenticationType === 'reentranceTicket' || system.authenticationType === 'oauth2' + ? ` (${t('texts.systemTypeBTP')})` + : ''; + + return `${system.name}${systemTypeName}${userDisplayName}`; +} + +// TODO: Replace with the function from btp-utils +/** + * + * @param destination + * @returns true if the destination is an ABAP OData destination + */ +export function isAbapODataDestination(destination: Destination): boolean { + return ( + !!destination.WebIDEUsage?.includes(WebIDEUsage.ODATA_ABAP) && + !destination.WebIDEUsage?.includes(WebIDEUsage.ODATA_GENERIC) + ); +} + +/** + * Matches the destination against the provided filters. Returns true if the destination matches any filters, false otherwise. + * + * @param destination + * @param filters + * @returns true if the destination matches any filters, false otherwise + */ +function matchesFilters(destination: Destination, filters?: DestinationFilters): boolean { + if (!filters) { + return true; + } + if (filters.odata_abap && isAbapODataDestination(destination)) { + return true; + } + + if (filters.full_service_url && isFullUrlDestination(destination)) { + return true; + } + + if (filters.partial_service_url && isPartialUrlDestination(destination)) { + return true; + } + LoggerHelper.logger.debug( + `Destination: ${ + destination.Name + } does not match any filters and will be excluded as a prompt choice. Destination configuration: ${JSON.stringify( + destination + )}` + ); + return false; +} + +/** + * Creates a list of choices for the system selection prompt using destinations or stored backend systems, depending on the environment. + * + * @param destinationFilters + * @returns a list of choices for the system selection prompt + */ +export async function createSystemChoices( + destinationFilters?: DestinationFilters +): Promise[]> { + let systemChoices: ListChoiceOptions[] = []; + let newSystemChoice: ListChoiceOptions; + + // If this is BAS, return destinations, otherwise return stored backend systems + if (isAppStudio()) { + const destinations = await listDestinations(); + systemChoices = Object.values(destinations) + .filter((destination) => { + return matchesFilters(destination, destinationFilters); + }) + .map((destination) => { + return { + name: destination.Name, + value: { + type: 'destination', + system: destination + } as SystemSelectionAnswerType + }; + }); + newSystemChoice = { name: t('prompts.newSystemType.choiceAbapOnBtp'), value: 'abapOnBtp' as SapSystemType }; // TODO: add new system choice for destinations + } else { + const backendSystems = await new SystemService(LoggerHelper.logger).getAll(); + systemChoices = backendSystems.map((system) => { + return { + name: getSystemDisplayName(system), + value: { + system, + type: 'backendSystem' + } as SystemSelectionAnswerType + }; + }); + newSystemChoice = { + name: t('prompts.systemSelection.newSystemChoiceLabel'), + value: { system: newSystemChoiceValue, type: 'newSystemChoice' } + }; + } + systemChoices.sort(({ name: nameA }, { name: nameB }) => + nameA!.localeCompare(nameB!, undefined, { numeric: true, caseFirst: 'lower' }) + ); + systemChoices.unshift(newSystemChoice); + return systemChoices; +} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts new file mode 100644 index 0000000000..52814f6a9c --- /dev/null +++ b/packages/odata-service-inquirer/src/prompts/datasources/sap-system/system-selection/questions.ts @@ -0,0 +1,240 @@ +import { Severity } from '@sap-devx/yeoman-ui-types'; +import type { Destination } from '@sap-ux/btp-utils'; +import { isAppStudio, isPartialUrlDestination } from '@sap-ux/btp-utils'; +import type { InputQuestion, ListQuestion } from '@sap-ux/inquirer-common'; +import { withCondition } from '@sap-ux/inquirer-common'; +import type { OdataVersion } from '@sap-ux/odata-service-writer'; +import type { BackendSystem } from '@sap-ux/store'; +import type { Answers, Question } from 'inquirer'; +import { t } from '../../../../i18n'; +import { hostEnvironment, type OdataServicePromptOptions } from '../../../../types'; +import { getHostEnvironment, PromptState } from '../../../../utils'; +import type { ValidationResult } from '../../../connectionValidator'; +import { ConnectionValidator } from '../../../connectionValidator'; +import LoggerHelper from '../../../logger-helper'; +import { BasicCredentialsPromptNames, getCredentialsPrompts } from '../credentials/questions'; +import { getNewSystemQuestions } from '../new-system/questions'; +import type { ServiceAnswer } from '../service-selection'; +import { getSystemServiceQuestion } from '../service-selection/questions'; +import { connectWithBackendSystem, connectWithDestination, createSystemChoices } from './prompt-helpers'; +import { validateUrl } from '@sap-ux/project-input-validator'; + +// New system choice value is a hard to guess string to avoid conflicts with existing system names or user named systems +// since it will be used as a new system value in the system selection prompt. +export const newSystemChoiceValue = '!@£*&937newSystem*X~qy^' as const; +type NewSystemChoice = typeof newSystemChoiceValue; + +const systemSelectionPromptNamespace = 'systemSelection'; + +const usernamePromptName = `${systemSelectionPromptNamespace}:${BasicCredentialsPromptNames.systemUsername}` as const; +const passwordPromptName = `${systemSelectionPromptNamespace}:${BasicCredentialsPromptNames.systemPassword}` as const; + +const systemSelectionPromptNames = { + systemSelection: 'systemSelection', + systemSelectionCli: 'systemSelectionCli', + destinationServicePath: 'destinationServicePath' +} as const; + +export type SystemSelectionAnswerType = { + type: 'destination' | 'backendSystem' | 'newSystemChoice'; + system: Destination | BackendSystem | NewSystemChoice; +}; + +interface SystemSelectionCredentialsAnswers { + [usernamePromptName]?: string; + [passwordPromptName]?: string; +} + +export interface SystemSelectionAnswers extends SystemSelectionCredentialsAnswers { + [systemSelectionPromptNames.systemSelection]?: SystemSelectionAnswerType; +} + +/** + * Validates the system selection, connecting to the selected system and validating the connection. + * + * @param systemSelection + * @param connectionValidator + * @param requiredOdataVersion + * @returns the validation result of the selected system connection attempt + */ +async function validateSystemSelection( + systemSelection: SystemSelectionAnswerType, + connectionValidator: ConnectionValidator, + requiredOdataVersion?: OdataVersion +): Promise { + if (systemSelection.type === 'newSystemChoice') { + return true; + } + let connectValResult: ValidationResult = false; + + if (systemSelection.type === 'backendSystem') { + connectValResult = await connectWithBackendSystem( + systemSelection.system as BackendSystem, + connectionValidator, + requiredOdataVersion + ); + } else if (systemSelection.type === 'destination') { + // Partial URL destinations will require additional service path prompt input, so we skip the connection validation here by returning true + // The service URL connection will need to be validated by the service path prompt + if (isPartialUrlDestination(systemSelection.system as Destination)) { + return true; + } + connectValResult = await connectWithDestination( + systemSelection.system as Destination, + connectionValidator, + requiredOdataVersion + ); + } + return connectValResult; +} + +/** + * Returns a list of questions for creating a new system configuration or selecting an existing stored system. + * + * @param promptOptions prompt options that may be used to customize the questions + * @returns a list of questions for creating a new system configuration or selecting an existing stored system + */ +export async function getSystemSelectionQuestions( + promptOptions?: OdataServicePromptOptions +): Promise> { + PromptState.reset(); + const connectValidator = new ConnectionValidator(); + + const questions: Question[] = await getSystemConnectionQuestions( + connectValidator, + promptOptions + ); + + // If the selected system was in fact not a system or a full service url but a partial url destination we require furthur service path input we need to add additional service path prompt + + // Existing system (BackendSystem or Destination) selected, TODO: make the service prompt optional by wrapping in condition `[promptOptions?.serviceSelection?.hide]` + questions.push( + ...(withCondition( + getSystemServiceQuestion(connectValidator, systemSelectionPromptNamespace, promptOptions) as Question[], + (answers: Answers) => (answers as SystemSelectionAnswers).systemSelection?.type !== 'newSystemChoice' + ) as Question[]) + ); + + // Create new system selected + questions.push( + ...(withCondition( + getNewSystemQuestions(promptOptions) as Question[], + (answers: Answers) => (answers as SystemSelectionAnswers).systemSelection?.type === 'newSystemChoice' + ) as Question[]) + ); + + return questions; +} + +/** + * Returns a list of existing systems, either destinations or backend systems from persistent store, depending on the environment. + * Note that destinations are only available in BAS environment and must include the destination attribute `WebIDEEnabled` to be listed. + * Additional destination attribute filters may be provided. + * + * @param connectionValidator A reference to the active connection validator, used to validate the service selection and retrieve service details. + * @param promptOptions + * @returns a list of existing systems + */ +export async function getSystemConnectionQuestions( + connectionValidator: ConnectionValidator, + promptOptions?: OdataServicePromptOptions +): Promise[]> { + const requiredOdataVersion = promptOptions?.serviceSelection?.requiredOdataVersion; + const destinationFilters = promptOptions?.systemSelection?.destinationFilters; + const systemChoices = await createSystemChoices(destinationFilters); + + const questions: Question[] = [ + { + type: 'list', + name: systemSelectionPromptNames.systemSelection, + message: t('prompts.systemSelection.message'), + // source: (preAnswers, input) => searchChoices(input, getSapSystemChoices(systems)), + choices: systemChoices, + validate: async (selectedSystem: SystemSelectionAnswerType): Promise => { + if (!selectedSystem) { + return false; + } + return validateSystemSelection(selectedSystem, connectionValidator, requiredOdataVersion) ?? false; + }, + additionalMessages: async (selectedSystem: SystemSelectionAnswerType) => { + // Backend systems credentials may need to be updated + if ( + selectedSystem.type === 'backendSystem' && + connectionValidator.systemAuthType === 'basic' && + (await connectionValidator.isAuthRequired()) + ) { + return { + message: t('prompts.systemSelection.authenticationFailedUpdateCredentials'), + severity: Severity.information + }; + } + } + } as ListQuestion + ]; + + if (isAppStudio()) { + const servicePathPrompt = { + when: (answers: SystemSelectionAnswers): boolean => { + const systemSelection = answers?.[systemSelectionPromptNames.systemSelection]; + if (systemSelection?.type === 'destination') { + return isPartialUrlDestination(systemSelection.system as Destination); + } + return false; + }, + type: 'input', + name: systemSelectionPromptNames.destinationServicePath, + message: t('prompts.destinationServicePath.message'), + guiOptions: { + hint: t('prompts.destinationServicePath.hint'), + mandatory: true, + breadcrumb: true + }, + validate: async (servicePath: string, answers: SystemSelectionAnswers) => { + if (!servicePath) { + return t('prompts.destinationServicePath.invalidServicePathWarning'); + } + // Validate format of the service path, note this relies on the assumption that the destination is correctly configured with a valid URL + const selectedDestination = answers?.[systemSelectionPromptNames.systemSelection] + ?.system as Destination; + const valUrlResult = validateUrl(selectedDestination.Host + servicePath); + if (valUrlResult !== true) { + return valUrlResult; + } + + const connectValResult = await connectWithDestination( + selectedDestination, + connectionValidator, + requiredOdataVersion, + servicePath + ); + return connectValResult; + } + } as InputQuestion; + questions.push(servicePathPrompt); + } + + // Only for CLI use as `list` prompt validation does not run on CLI + if (getHostEnvironment() === hostEnvironment.cli) { + questions.push({ + when: async (answers: Answers): Promise => { + const systemSelection = answers?.[systemSelectionPromptNames.systemSelection]; + const connectValResult = await validateSystemSelection( + systemSelection, + connectionValidator, + requiredOdataVersion + ); + // An issue occurred with the selected system, there is no need to continue on the CLI, log and exit + if (connectValResult !== true) { + LoggerHelper.logger.error(connectValResult.toString); + throw new Error(connectValResult.toString()); + } + return false; + }, + name: `${systemSelectionPromptNames.systemSelectionCli}` + } as Question); + } + const credentialsPrompts = getCredentialsPrompts(connectionValidator, systemSelectionPromptNamespace) as Question[]; + questions.push(...credentialsPrompts); + + return questions; +} diff --git a/packages/odata-service-inquirer/src/prompts/datasources/service-url/questions.ts b/packages/odata-service-inquirer/src/prompts/datasources/service-url/questions.ts index 80f5bd663f..27ce8848c3 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/service-url/questions.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/service-url/questions.ts @@ -219,9 +219,10 @@ function getPasswordPrompt( applyDefaultWhenDirty: true, mandatory: true }, + guiType: 'login', name: promptNames.serviceUrlPassword, message: t('prompts.servicePassword.message'), - guiType: 'login', + //guiType: 'login', mask: '*', validate: async (password: string, { username, serviceUrl, ignoreCertError, sapClient }: ServiceUrlAnswers) => { if (!serviceUrl || !username || !password) { diff --git a/packages/odata-service-inquirer/src/prompts/datasources/service-url/validators.ts b/packages/odata-service-inquirer/src/prompts/datasources/service-url/validators.ts index f6d8891b56..13b1d6ac00 100644 --- a/packages/odata-service-inquirer/src/prompts/datasources/service-url/validators.ts +++ b/packages/odata-service-inquirer/src/prompts/datasources/service-url/validators.ts @@ -7,6 +7,7 @@ import { PromptState, originToRelative, parseOdataVersion } from '../../../utils import { ConnectionValidator } from '../../connectionValidator'; import LoggerHelper from '../../logger-helper'; import { errorHandler } from '../../prompt-helpers'; +// TODO: Much of this code is replicated in service-selection/questions.ts. Consider refactoring to a shared location. /** * Validates that a service specified by the service url is accessible, has the required version and returns valid metadata. @@ -30,6 +31,7 @@ export async function validateService( if (ignoreCertError === true) { ConnectionValidator.setGlobalRejectUnauthorized(!ignoreCertError); } + // todo: Replace with `validateODataVersion` const metadata = await odataService.metadata(); const serviceOdataVersion = parseOdataVersion(metadata); diff --git a/packages/odata-service-inquirer/src/prompts/prompts.ts b/packages/odata-service-inquirer/src/prompts/prompts.ts index 6670b059da..b739d55c10 100644 --- a/packages/odata-service-inquirer/src/prompts/prompts.ts +++ b/packages/odata-service-inquirer/src/prompts/prompts.ts @@ -12,8 +12,7 @@ import { } from '../types'; import { getLocalCapProjectPrompts } from './datasources/cap-project/questions'; import { getMetadataFileQuestion } from './datasources/metadata-file'; -import type { SystemSelectionAnswer } from './datasources/sap-system/new-system/questions'; -import { getNewSystemQuestions, newSystemChoiceValue } from './datasources/sap-system/new-system/questions'; +import { getSystemSelectionQuestions } from './datasources/sap-system/system-selection'; import { getServiceUrlQuestions } from './datasources/service-url/questions'; import LoggerHelper from './logger-helper'; import { getDatasourceTypeChoices } from './prompt-helpers'; @@ -55,8 +54,8 @@ function getDatasourceTypeQuestion(options?: DatasourceTypePromptOptions): YUIQu [ DatasourceType.businessHub, DatasourceType.none, - DatasourceType.projectSpecificDestination, - DatasourceType.sapSystem + DatasourceType.projectSpecificDestination + // DatasourceType.sapSystem ].includes(source) ) { LoggerHelper.logger?.warn( @@ -90,6 +89,13 @@ async function getDatasourceTypeConditionalQuestions( ): Promise { const conditionalQuestions: OdataServiceQuestion[] = []; + conditionalQuestions.push( + ...(withCondition( + (await getSystemSelectionQuestions(promptOptions)) as Question[], + (answers: Answers) => (answers as OdataServiceAnswers).datasourceType === DatasourceType.sapSystem + ) as OdataServiceQuestion[]) + ); + conditionalQuestions.push( ...(withCondition( [getMetadataFileQuestion(promptOptions?.metadataFilePath) as Question], @@ -111,16 +117,5 @@ async function getDatasourceTypeConditionalQuestions( ) as OdataServiceQuestion[]) ); - conditionalQuestions.push( - ...(withCondition( - getNewSystemQuestions(promptOptions) as Question[], - (answers: Answers) => - (answers as OdataServiceAnswers).datasourceType === DatasourceType.sapSystem && - (answers as SystemSelectionAnswer).system === newSystemChoiceValue - ) as OdataServiceQuestion[]) - ); - - //...further data sources to be added here - return conditionalQuestions; } diff --git a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json index d2a7229680..458b9ba9fb 100644 --- a/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json +++ b/packages/odata-service-inquirer/src/translations/odata-service-inquirer.i18n.json @@ -81,7 +81,7 @@ "message": "Password" }, "systemService": { - "message": "Service name", + "message": "Service", "breadcrumb": "Service", "noServicesWarning": "No services available for the selected system, see logs for further details." }, @@ -99,7 +99,9 @@ "emptySystemNameWarning": "System name cannot be empty." }, "systemSelection": { - "newSystemChoiceLabel": "New system" + "newSystemChoiceLabel": "New system", + "message": "Select a system configuration", + "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials." }, "abapOnBTPType": { "message": "ABAP environment definition source", @@ -116,6 +118,11 @@ "cloudFoundryAbapSystem": { "message": "ABAP environment", "hint": "Enter the name of the Cloud Foundry service that contains the ABAP Environment instance" + }, + "destinationServicePath": { + "message": "Service path", + "hint": "Enter the path to the odata service, relative to the selected destination URL", + "invalidServicePathWarning": "Please enter a valid service path" } }, "errors": { @@ -126,11 +133,12 @@ "servicesUnavailable": "An error occurred retrieving service(s) for SAP System.", "certificateError": "A certificate error has occurred: {{- error}}", "urlCertValidationError": "The system URL is using {{certErrorReason}} security certificate.", - "authenticationFailed": "Authentication incorrect {{error}}", + "authenticationFailed": "Authentication incorrect. {{error}}", "authenticationTimeout": "Authorization was not verified within the allowed time. Please ensure you have authenticated using the associated browser window.", "invalidUrl": "Not a valid URL", "connectionError": "A connection error occurred, please ensure the target host is available on the network: {{- error}}", "timeout": "A connection timeout error occurred: {{- error}}", + "badRequest": "The server returned an error: bad request, please check the URL and try again.", "serviceUnavailable": "Selected service is returning an error.", "catalogServiceNotActive": "Catalog service is not active", "internalServerError": "The URL you have provided cannot be accessed and is returning: '{{- error}}'. Please ensure that the URL is accessible externally.", @@ -145,7 +153,7 @@ "abapEnvsUnavailable": "ABAP environments unavailable", "noSuchHostError": "No such host is known", "odataServiceVersionMismatch": "The template you have chosen supports V{{requiredVersion}} OData services only. The provided version is V{{serviceVersion}}.", - "destinationAuthError": "The selected system is returning an authentication error. Please verify the destination configuration", + "destinationAuthError": "The selected system is returning an authentication error. System name: {{systemName}}, error: {{- error}}", "systemOrServiceUrlNotFound": "Please verify the url: {{- url}}, target system configuration and network connectivity", "urlRedirect": "The service URL is redirecting", "certValidationRequired": "Certificate validation is required to continue.", @@ -157,7 +165,9 @@ "noAbapEnvsInCFSpace": "No ABAP environments in CF space found.", "abapEnvsCFDiscoveryFailed": "Discovering ABAP Environments failed. Please ensure you are logged into Cloud Foundry (see https://docs.cloudfoundry.org/cf-cli/getting-started.html#login).", "abapServiceAuthenticationFailed": "ABAP environment authentication using UAA failed.", - "serviceCatalogRequest": "An error occurred requesting services from: {{- catalogRequestUri }} and entity set: {{entitySet}}. {{error}}" + "serviceCatalogRequest": "An error occurred requesting services from: {{- catalogRequestUri }} and entity set: {{entitySet}}. {{error}}", + "storedSystemConnectionError": "An error occurred while validating the stored system connection info. System name: {{systemName}}, error: {{- error}}", + "noCatalogOrServiceAvailable": "No active system or odata service endpoint connection available to retrieve service(s)." }, "texts": { "anExpiredCert": "an expired", @@ -166,7 +176,9 @@ "anUntrustedRootCert": "an untrusted root", "suggestedSystemNameClient": ", client {{client}}", "seeLogForDetails": "See log for more details.", - "forUserName": "(for user [{{username}}])" + "forUserName": "(for user [{{username}}])", + "systemTypeBTP": "BTP", + "systemTypeS4HC": "S4HC" }, "guidedAnswers": { "validationErrorHelpText": "Need help with this error?" diff --git a/packages/odata-service-inquirer/src/types.ts b/packages/odata-service-inquirer/src/types.ts index 658dc9675c..0360a4d4f2 100644 --- a/packages/odata-service-inquirer/src/types.ts +++ b/packages/odata-service-inquirer/src/types.ts @@ -5,6 +5,7 @@ import type { OdataVersion } from '@sap-ux/odata-service-writer'; import type { CdsVersionInfo } from '@sap-ux/project-access'; import type { ListChoiceOptions } from 'inquirer'; import type { BackendSystem } from '@sap-ux/store'; +import type { Destination } from '@sap-ux/btp-utils'; /** * This file contains types that are exported by the module and are needed for consumers using the APIs `prompt` and `getPrompts`. @@ -89,8 +90,14 @@ export interface OdataServiceAnswers { /** * The persistable backend system representation of the connected service provider + * `newOrUpdated` is set to true if the system was newly created or updated during the connection validation process and should be considered for storage. */ - backendSystem?: BackendSystem; + backendSystem?: BackendSystem & { newOrUpdated?: boolean }; + + /** + * The destination information for the connected system + */ + destination?: Destination; }; } @@ -129,7 +136,11 @@ export enum promptNames { /** * Newly created systems can be named for storage */ - userSystemName = 'userSystemName' + userSystemName = 'userSystemName', + /** + * System selection + */ + systemSelection = 'systemSelection' } export type CapRuntime = 'Node.js' | 'Java'; @@ -209,6 +220,44 @@ export type DatasourceTypePromptOptions = { choices?: DatasourceType[]; }; +export type DestinationFilters = { + /** + * 'WebIDEUsage' property is defined and includes the value 'odata_abap' and does not includes the value 'odata_gen' + */ + odata_abap: boolean; + /** + * 'WebIDEUsage' property is defined and includes the value 'odata_gen' and does not includes the value 'odata_abap'. + */ + odata_generic: boolean; + /** + * 'WebIDEAdditionalData' property is defined and includes the value 'full_url' and + * 'WebIDEUsage' property is defined and includes the value 'odata_gen' and does not includes the value 'odata_abap'. + */ + full_service_url: boolean; + /** + * 'WebIDEAdditionalData' property is defined and does not include the value 'full_url' and + * 'WebIDEUsage' property is defined and includes the value 'odata_gen' and does not includes the value 'odata_abap'. + */ + partial_service_url: boolean; + /** + * todo: Add implementation + * Abap cloud destination + */ + abap_cloud: boolean; + /** + * Abap on-premise destination + */ + abap_on_premise: boolean; +}; + +export type SystemSelectionPromptOptions = { + /** + * Set the specific filter option(s) to true to include only the destinatons that have matching configuration attributes. + * If no filter is set, all destinations will be included. If multiple filters are set, the destination will be included if it matches any of the filters. + */ + destinationFilters: DestinationFilters; +}; + export type MetadataPromptOptions = { /** * Used to validate the metadata file contains the required odata version edmx @@ -255,7 +304,8 @@ type odataServiceInquirerPromptOptions = Record & Record & Record & - Record; + Record & + Record; export type OdataServiceQuestion = YUIQuestion; diff --git a/packages/odata-service-inquirer/test/unit/__snapshots__/index-api.test.ts.snap b/packages/odata-service-inquirer/test/unit/__snapshots__/index-api.test.ts.snap new file mode 100644 index 0000000000..cbe4585322 --- /dev/null +++ b/packages/odata-service-inquirer/test/unit/__snapshots__/index-api.test.ts.snap @@ -0,0 +1,394 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`API tests getPrompts, i18n is loaded 1`] = ` +[ + { + "additionalMessages": [Function], + "choices": [ + { + "name": "Connect to a SAP System", + "value": "sapSystem", + }, + { + "name": "Connect to an OData Service Url", + "value": "odataServiceUrl", + }, + { + "name": "Connect to SAP Business Accelerator Hub", + "value": "businessHub", + }, + { + "name": "Use a Local CAP Project", + "value": "capProject", + }, + { + "name": "Upload a Metadata File", + "value": "metadataFile", + }, + ], + "default": -1, + "guiOptions": { + "breadcrumb": true, + }, + "message": "Data source", + "name": "datasourceType", + "type": "list", + }, + { + "additionalMessages": [Function], + "choices": [ + { + "name": "New system", + "value": { + "system": "!@£*&937newSystem*X~qy^", + "type": "newSystemChoice", + }, + }, + { + "name": "storedSystem1", + "value": { + "system": { + "name": "storedSystem1", + "url": "http://url1", + }, + "type": "backendSystem", + }, + }, + { + "name": "storedSystem2", + "value": { + "system": { + "name": "storedSystem2", + "url": "http://url2", + }, + "type": "backendSystem", + }, + }, + ], + "message": "Select a system configuration", + "name": "systemSelection", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "message": "Username", + "name": "systemSelection:systemUsername", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Password", + "name": "systemSelection:systemPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, + { + "name": "systemSelectionCli", + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "systemSelection:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "choices": [ + { + "name": "ABAP Environment on SAP Business Technology Platform", + "value": "abapOnBtp", + }, + { + "name": "ABAP On Premise", + "value": "abapOnPrem", + }, + ], + "message": "System type", + "name": "newSystemType", + "type": "list", + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "Enter the URL of the SAP System", + "mandatory": true, + }, + "message": "System URL", + "name": "abapOnPrem:newSystemUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": "SAP Client", + }, + "message": "SAP client (leave empty for default)", + "name": "sapClient", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "message": "Username", + "name": "abapOnPrem:systemUsername", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Password", + "name": "abapOnPrem:systemPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "hint": "Entering a system name will save the connection for re-use.", + "mandatory": true, + }, + "message": "System name", + "name": "abapOnPrem:userSystemName", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "abapOnPrem:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "choices": [ + { + "name": "Discover a Cloud Foundry Service", + "value": "cloudFoundry", + }, + { + "name": "Upload a Service Key File", + "value": "serviceKey", + }, + { + "name": "Use Reentrance Ticket", + "value": "reentranceTicket", + }, + ], + "message": "ABAP environment definition source", + "name": "abapOnBtpAuthType", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "Enter the URL of the SAP System", + "mandatory": true, + }, + "message": "System URL", + "name": "abapOnBtp:newSystemUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "hint": "Select a local file that defines the service connection for an ABAP Environment on SAP Business Technology Platform", + "mandatory": true, + }, + "guiType": "file-browser", + "message": "Service key file path", + "name": "serviceKey", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + }, + "message": "ABAP environment", + "name": "cloudFoundryAbapSystem", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "hint": "Entering a system name will save the connection for re-use.", + "mandatory": true, + }, + "message": "System name", + "name": "abapOnBtp:userSystemName", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "abapOnBtp:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "mandatory": true, + }, + "guiType": "file-browser", + "message": "Metadata file path", + "name": "metadataFilePath", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "CAP Project", + "mandatory": true, + }, + "message": "Choose your CAP project", + "name": "capProject", + "type": "list", + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "breadcrumb": "CAP Project", + "mandatory": true, + }, + "guiType": "folder-browser", + "message": "CAP project folder path", + "name": "capProjectPath", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "mandatory": true, + }, + "message": "OData service", + "name": "capService", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "https://:/path/to/odata/service/", + "mandatory": true, + }, + "message": "OData service URL", + "name": "serviceUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": false, + "message": "Do you want to continue generation with the untrusted certificate?", + "name": "ignoreCertError", + "type": "confirm", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "mandatory": true, + }, + "message": "Service username", + "name": "username", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "applyDefaultWhenDirty": true, + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Service password", + "name": "serviceUrlPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, +] +`; diff --git a/packages/odata-service-inquirer/test/unit/index-api.test.ts b/packages/odata-service-inquirer/test/unit/index-api.test.ts index b540549281..8c8a1cfb9e 100644 --- a/packages/odata-service-inquirer/test/unit/index-api.test.ts +++ b/packages/odata-service-inquirer/test/unit/index-api.test.ts @@ -5,12 +5,30 @@ import * as utils from '../../src/utils'; import LoggerHelper from '../../src/prompts/logger-helper'; import { PromptState } from '../../src/utils'; import { hostEnvironment } from '../../src/types'; +import { type BackendSystem } from '@sap-ux/store'; jest.mock('../../src/prompts', () => ({ __esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property ...jest.requireActual('../../src/prompts') })); +jest.mock('@sap-ux/store', () => ({ + __esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property + ...jest.requireActual('@sap-ux/store'), + SystemService: jest.fn().mockImplementation(() => ({ + getAll: jest.fn().mockResolvedValue([ + { + name: 'storedSystem1', + url: 'http://url1' + }, + { + name: 'storedSystem2', + url: 'http://url2' + } + ] as BackendSystem[]) + })) +})); + describe('API tests', () => { beforeEach(() => { jest.restoreAllMocks(); @@ -42,320 +60,6 @@ describe('API tests', () => { jest.spyOn(utils, 'getHostEnvironment').mockReturnValueOnce(hostEnvironment.cli); const { prompts: questions } = await getPrompts(undefined, undefined, true, undefined, true); - expect(questions).toMatchInlineSnapshot(` - [ - { - "additionalMessages": [Function], - "choices": [ - { - "name": "Connect to a SAP System", - "value": "sapSystem", - }, - { - "name": "Connect to an OData Service Url", - "value": "odataServiceUrl", - }, - { - "name": "Connect to SAP Business Accelerator Hub", - "value": "businessHub", - }, - { - "name": "Use a Local CAP Project", - "value": "capProject", - }, - { - "name": "Upload a Metadata File", - "value": "metadataFile", - }, - ], - "default": -1, - "guiOptions": { - "breadcrumb": true, - }, - "message": "Data source", - "name": "datasourceType", - "type": "list", - }, - { - "guiOptions": { - "breadcrumb": true, - "mandatory": true, - }, - "guiType": "file-browser", - "message": "Metadata file path", - "name": "metadataFilePath", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "CAP Project", - "mandatory": true, - }, - "message": "Choose your CAP project", - "name": "capProject", - "type": "list", - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "breadcrumb": "CAP Project", - "mandatory": true, - }, - "guiType": "folder-browser", - "message": "CAP project folder path", - "name": "capProjectPath", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "mandatory": true, - }, - "message": "OData service", - "name": "capService", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "name": "capCliStateSetter", - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "https://:/path/to/odata/service/", - "mandatory": true, - }, - "message": "OData service URL", - "name": "serviceUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": false, - "message": "Do you want to continue generation with the untrusted certificate?", - "name": "ignoreCertError", - "type": "confirm", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "mandatory": true, - }, - "message": "Service username", - "name": "username", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "applyDefaultWhenDirty": true, - "mandatory": true, - }, - "guiType": "login", - "mask": "*", - "message": "Service password", - "name": "serviceUrlPassword", - "type": "password", - "validate": [Function], - "when": [Function], - }, - { - "choices": [ - { - "name": "ABAP Environment on SAP Business Technology Platform", - "value": "abapOnBtp", - }, - { - "name": "ABAP On Premise", - "value": "abapOnPrem", - }, - ], - "message": "System type", - "name": "newSystemType", - "type": "list", - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "Enter the URL of the SAP System", - "mandatory": true, - }, - "message": "System URL", - "name": "abapOnPrem:newSystemUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": "SAP Client", - }, - "message": "SAP client (leave empty for default)", - "name": "sapClient", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": "", - "guiOptions": { - "mandatory": true, - }, - "message": "Username", - "name": "abapSystemUsername", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": "", - "guiOptions": { - "mandatory": true, - }, - "guiType": "login", - "mask": "*", - "message": "Password", - "name": "abapSystemPassword", - "type": "password", - "validate": [Function], - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "hint": "Entering a system name will save the connection for re-use.", - "mandatory": true, - }, - "message": "System name", - "name": "abapOnPrem:userSystemName", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "additionalMessages": [Function], - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "Service", - "mandatory": true, - }, - "message": [Function], - "name": "abapOnPrem:serviceSelection", - "source": [Function], - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "choices": [ - { - "name": "Discover a Cloud Foundry Service", - "value": "cloudFoundry", - }, - { - "name": "Upload a Service Key File", - "value": "serviceKey", - }, - { - "name": "Use Reentrance Ticket", - "value": "reentranceTicket", - }, - ], - "message": "ABAP environment definition source", - "name": "abapOnBtpAuthType", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "Enter the URL of the SAP System", - "mandatory": true, - }, - "message": "System URL", - "name": "abapOnBtp:newSystemUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "hint": "Select a local file that defines the service connection for an ABAP Environment on SAP Business Technology Platform", - "mandatory": true, - }, - "guiType": "file-browser", - "message": "Service key file path", - "name": "serviceKey", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - }, - "message": "ABAP environment", - "name": "cloudFoundryAbapSystem", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "hint": "Entering a system name will save the connection for re-use.", - "mandatory": true, - }, - "message": "System name", - "name": "abapOnBtp:userSystemName", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "additionalMessages": [Function], - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "Service", - "mandatory": true, - }, - "message": [Function], - "name": "abapOnBtp:serviceSelection", - "source": [Function], - "type": "list", - "validate": [Function], - "when": [Function], - }, - ] - `); + expect(questions).toMatchSnapshot(); }); }); diff --git a/packages/odata-service-inquirer/test/unit/prompts/__snapshots__/prompts.test.ts.snap b/packages/odata-service-inquirer/test/unit/prompts/__snapshots__/prompts.test.ts.snap new file mode 100644 index 0000000000..0c64dabd94 --- /dev/null +++ b/packages/odata-service-inquirer/test/unit/prompts/__snapshots__/prompts.test.ts.snap @@ -0,0 +1,418 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getQuestions getQuestions 1`] = ` +[ + { + "additionalMessages": [Function], + "choices": [ + { + "name": "Connect to a SAP System", + "value": "sapSystem", + }, + { + "name": "Connect to an OData Service Url", + "value": "odataServiceUrl", + }, + { + "name": "Connect to SAP Business Accelerator Hub", + "value": "businessHub", + }, + { + "name": "Use a Local CAP Project", + "value": "capProject", + }, + { + "name": "Upload a Metadata File", + "value": "metadataFile", + }, + ], + "default": -1, + "guiOptions": { + "breadcrumb": true, + }, + "message": "Data source", + "name": "datasourceType", + "type": "list", + }, + { + "additionalMessages": [Function], + "choices": [ + { + "name": "New system", + "value": { + "system": "!@£*&937newSystem*X~qy^", + "type": "newSystemChoice", + }, + }, + { + "name": "storedSystem1", + "value": { + "system": { + "name": "storedSystem1", + "url": "http://url1", + }, + "type": "backendSystem", + }, + }, + { + "name": "storedSystem2", + "value": { + "system": { + "name": "storedSystem2", + "url": "http://url2", + }, + "type": "backendSystem", + }, + }, + ], + "message": "Select a system configuration", + "name": "systemSelection", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "message": "Username", + "name": "systemSelection:systemUsername", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Password", + "name": "systemSelection:systemPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, + { + "name": "systemSelectionCli", + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "systemSelection:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "name": "systemSelection:cliServiceSelection", + "when": [Function], + }, + { + "choices": [ + { + "name": "ABAP Environment on SAP Business Technology Platform", + "value": "abapOnBtp", + }, + { + "name": "ABAP On Premise", + "value": "abapOnPrem", + }, + ], + "message": "System type", + "name": "newSystemType", + "type": "list", + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "Enter the URL of the SAP System", + "mandatory": true, + }, + "message": "System URL", + "name": "abapOnPrem:newSystemUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": "SAP Client", + }, + "message": "SAP client (leave empty for default)", + "name": "sapClient", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "message": "Username", + "name": "abapOnPrem:systemUsername", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": "", + "guiOptions": { + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Password", + "name": "abapOnPrem:systemPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "hint": "Entering a system name will save the connection for re-use.", + "mandatory": true, + }, + "message": "System name", + "name": "abapOnPrem:userSystemName", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "abapOnPrem:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "name": "abapOnPrem:cliServiceSelection", + "when": [Function], + }, + { + "choices": [ + { + "name": "Discover a Cloud Foundry Service", + "value": "cloudFoundry", + }, + { + "name": "Upload a Service Key File", + "value": "serviceKey", + }, + { + "name": "Use Reentrance Ticket", + "value": "reentranceTicket", + }, + ], + "message": "ABAP environment definition source", + "name": "abapOnBtpAuthType", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "Enter the URL of the SAP System", + "mandatory": true, + }, + "message": "System URL", + "name": "abapOnBtp:newSystemUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "hint": "Select a local file that defines the service connection for an ABAP Environment on SAP Business Technology Platform", + "mandatory": true, + }, + "guiType": "file-browser", + "message": "Service key file path", + "name": "serviceKey", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + }, + "message": "ABAP environment", + "name": "cloudFoundryAbapSystem", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "name": "cliCfAbapService", + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "hint": "Entering a system name will save the connection for re-use.", + "mandatory": true, + }, + "message": "System name", + "name": "abapOnBtp:userSystemName", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "additionalMessages": [Function], + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "Service", + "mandatory": true, + }, + "message": [Function], + "name": "abapOnBtp:serviceSelection", + "source": [Function], + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "name": "abapOnBtp:cliServiceSelection", + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "mandatory": true, + }, + "guiType": "file-browser", + "message": "Metadata file path", + "name": "metadataFilePath", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": "CAP Project", + "mandatory": true, + }, + "message": "Choose your CAP project", + "name": "capProject", + "type": "list", + "when": [Function], + }, + { + "default": [Function], + "guiOptions": { + "breadcrumb": "CAP Project", + "mandatory": true, + }, + "guiType": "folder-browser", + "message": "CAP project folder path", + "name": "capProjectPath", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "choices": [Function], + "default": [Function], + "guiOptions": { + "applyDefaultWhenDirty": true, + "breadcrumb": true, + "mandatory": true, + }, + "message": "OData service", + "name": "capService", + "type": "list", + "validate": [Function], + "when": [Function], + }, + { + "name": "capCliStateSetter", + "when": [Function], + }, + { + "guiOptions": { + "breadcrumb": true, + "hint": "https://:/path/to/odata/service/", + "mandatory": true, + }, + "message": "OData service URL", + "name": "serviceUrl", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "default": false, + "message": "Do you want to continue generation with the untrusted certificate?", + "name": "ignoreCertError", + "type": "confirm", + "validate": [Function], + "when": [Function], + }, + { + "name": "cliIgnoreCertValidate", + "when": [Function], + }, + { + "guiOptions": { + "mandatory": true, + }, + "message": "Service username", + "name": "username", + "type": "input", + "validate": [Function], + "when": [Function], + }, + { + "guiOptions": { + "applyDefaultWhenDirty": true, + "mandatory": true, + }, + "guiType": "login", + "mask": "*", + "message": "Service password", + "name": "serviceUrlPassword", + "type": "password", + "validate": [Function], + "when": [Function], + }, +] +`; diff --git a/packages/odata-service-inquirer/test/unit/prompts/connectionValidator.test.ts b/packages/odata-service-inquirer/test/unit/prompts/connectionValidator.test.ts index 145031946e..e27b028c1b 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/connectionValidator.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/connectionValidator.test.ts @@ -62,9 +62,7 @@ describe('ConnectionValidator', () => { const result = await validator.validateUrl(invalidUrl); expect(result).toBe(t('errors.invalidUrl')); - expect(validator.validity).toEqual({ - urlFormat: false - }); + expect(validator.validity).toEqual({}); expect(await validator.validateUrl('')).toBe(false); expect(validator.validity).toEqual({ @@ -403,8 +401,8 @@ describe('ConnectionValidator', () => { expect(getOdataServiceSpy).toHaveBeenCalled(); getOdataServiceSpy.mockClear(); - // Auth is required even though a 200 since the url initially returned 401 - expect(await connectValidator.isAuthRequired('https://example.com/serviceA', '999')).toBe(true); + // Auth is not required since the connection has been authenticated + expect(await connectValidator.isAuthRequired('https://example.com/serviceA', '999')).toBe(false); // Should not recheck with the same url and client expect(getOdataServiceSpy).not.toHaveBeenCalled(); diff --git a/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts b/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts index 5450d0298e..70e77fdcdb 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts @@ -5,6 +5,9 @@ import * as utils from '../../../src/utils'; import * as btpUtils from '@sap-ux/btp-utils'; import { Severity } from '@sap-devx/yeoman-ui-types'; import { ToolsLogger } from '@sap-ux/logger'; +import type { BackendSystem } from '@sap-ux/store'; +import { getService } from '@sap-ux/store'; +import { Service } from '@sap-ux/axios-extension'; /** * Workaround to for spyOn TypeError: Jest cannot redefine property @@ -16,6 +19,23 @@ jest.mock('@sap-ux/btp-utils', () => { }; }); +jest.mock('@sap-ux/store', () => ({ + __esModule: true, // Workaround to for spyOn TypeError: Jest cannot redefine property + ...jest.requireActual('@sap-ux/store'), + SystemService: jest.fn().mockImplementation(() => ({ + getAll: jest.fn().mockResolvedValue([ + { + name: 'storedSystem1', + url: 'http://url1' + }, + { + name: 'storedSystem2', + url: 'http://url2' + } + ] as BackendSystem[]) + })) +})); + describe('getQuestions', () => { beforeAll(async () => { // Wait for i18n to bootstrap so we can test localised strings @@ -29,337 +49,7 @@ describe('getQuestions', () => { test('getQuestions', async () => { jest.spyOn(utils, 'getHostEnvironment').mockReturnValueOnce(hostEnvironment.cli); // Tests all declaritive values - expect(await getQuestions()).toMatchInlineSnapshot(` - [ - { - "additionalMessages": [Function], - "choices": [ - { - "name": "Connect to a SAP System", - "value": "sapSystem", - }, - { - "name": "Connect to an OData Service Url", - "value": "odataServiceUrl", - }, - { - "name": "Connect to SAP Business Accelerator Hub", - "value": "businessHub", - }, - { - "name": "Use a Local CAP Project", - "value": "capProject", - }, - { - "name": "Upload a Metadata File", - "value": "metadataFile", - }, - ], - "default": -1, - "guiOptions": { - "breadcrumb": true, - }, - "message": "Data source", - "name": "datasourceType", - "type": "list", - }, - { - "guiOptions": { - "breadcrumb": true, - "mandatory": true, - }, - "guiType": "file-browser", - "message": "Metadata file path", - "name": "metadataFilePath", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "CAP Project", - "mandatory": true, - }, - "message": "Choose your CAP project", - "name": "capProject", - "type": "list", - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "breadcrumb": "CAP Project", - "mandatory": true, - }, - "guiType": "folder-browser", - "message": "CAP project folder path", - "name": "capProjectPath", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "mandatory": true, - }, - "message": "OData service", - "name": "capService", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "name": "capCliStateSetter", - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "https://:/path/to/odata/service/", - "mandatory": true, - }, - "message": "OData service URL", - "name": "serviceUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": false, - "message": "Do you want to continue generation with the untrusted certificate?", - "name": "ignoreCertError", - "type": "confirm", - "validate": [Function], - "when": [Function], - }, - { - "name": "cliIgnoreCertValidate", - "when": [Function], - }, - { - "guiOptions": { - "mandatory": true, - }, - "message": "Service username", - "name": "username", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "applyDefaultWhenDirty": true, - "mandatory": true, - }, - "guiType": "login", - "mask": "*", - "message": "Service password", - "name": "serviceUrlPassword", - "type": "password", - "validate": [Function], - "when": [Function], - }, - { - "choices": [ - { - "name": "ABAP Environment on SAP Business Technology Platform", - "value": "abapOnBtp", - }, - { - "name": "ABAP On Premise", - "value": "abapOnPrem", - }, - ], - "message": "System type", - "name": "newSystemType", - "type": "list", - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "Enter the URL of the SAP System", - "mandatory": true, - }, - "message": "System URL", - "name": "abapOnPrem:newSystemUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": "SAP Client", - }, - "message": "SAP client (leave empty for default)", - "name": "sapClient", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": "", - "guiOptions": { - "mandatory": true, - }, - "message": "Username", - "name": "abapSystemUsername", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "default": "", - "guiOptions": { - "mandatory": true, - }, - "guiType": "login", - "mask": "*", - "message": "Password", - "name": "abapSystemPassword", - "type": "password", - "validate": [Function], - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "hint": "Entering a system name will save the connection for re-use.", - "mandatory": true, - }, - "message": "System name", - "name": "abapOnPrem:userSystemName", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "additionalMessages": [Function], - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "Service", - "mandatory": true, - }, - "message": [Function], - "name": "abapOnPrem:serviceSelection", - "source": [Function], - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "name": "abapOnPrem:cliServiceSelection", - "when": [Function], - }, - { - "choices": [ - { - "name": "Discover a Cloud Foundry Service", - "value": "cloudFoundry", - }, - { - "name": "Upload a Service Key File", - "value": "serviceKey", - }, - { - "name": "Use Reentrance Ticket", - "value": "reentranceTicket", - }, - ], - "message": "ABAP environment definition source", - "name": "abapOnBtpAuthType", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "breadcrumb": true, - "hint": "Enter the URL of the SAP System", - "mandatory": true, - }, - "message": "System URL", - "name": "abapOnBtp:newSystemUrl", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "guiOptions": { - "hint": "Select a local file that defines the service connection for an ABAP Environment on SAP Business Technology Platform", - "mandatory": true, - }, - "guiType": "file-browser", - "message": "Service key file path", - "name": "serviceKey", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - }, - "message": "ABAP environment", - "name": "cloudFoundryAbapSystem", - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "name": "cliCfAbapService", - "when": [Function], - }, - { - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": true, - "hint": "Entering a system name will save the connection for re-use.", - "mandatory": true, - }, - "message": "System name", - "name": "abapOnBtp:userSystemName", - "type": "input", - "validate": [Function], - "when": [Function], - }, - { - "additionalMessages": [Function], - "choices": [Function], - "default": [Function], - "guiOptions": { - "applyDefaultWhenDirty": true, - "breadcrumb": "Service", - "mandatory": true, - }, - "message": [Function], - "name": "abapOnBtp:serviceSelection", - "source": [Function], - "type": "list", - "validate": [Function], - "when": [Function], - }, - { - "name": "abapOnBtp:cliServiceSelection", - "when": [Function], - }, - ] - `); + expect(await getQuestions()).toMatchSnapshot(); // Test that default is correctly set by options expect((await getQuestions({ datasourceType: { default: DatasourceType.capProject } }))[0]).toMatchObject({ @@ -388,20 +78,17 @@ describe('getQuestions', () => { const datasourceTypeQuestion = (await getQuestions())[0]; expect(datasourceTypeQuestion.name).toEqual('datasourceType'); - [ - DatasourceType.businessHub, - DatasourceType.none, - DatasourceType.projectSpecificDestination, - DatasourceType.sapSystem - ].forEach((datasourceType) => { - const additionalMessages = (datasourceTypeQuestion.additionalMessages as Function)(datasourceType); - expect(additionalMessages).toMatchObject({ - message: t('prompts.datasourceType.notYetImplementedWarningMessage', { datasourceType }), - severity: Severity.warning - }); - expect(logWarnSpy).toHaveBeenCalledWith( - t('prompts.datasourceType.notYetImplementedWarningMessage', { datasourceType }) - ); - }); + [DatasourceType.businessHub, DatasourceType.none, DatasourceType.projectSpecificDestination].forEach( + (datasourceType) => { + const additionalMessages = (datasourceTypeQuestion.additionalMessages as Function)(datasourceType); + expect(additionalMessages).toMatchObject({ + message: t('prompts.datasourceType.notYetImplementedWarningMessage', { datasourceType }), + severity: Severity.warning + }); + expect(logWarnSpy).toHaveBeenCalledWith( + t('prompts.datasourceType.notYetImplementedWarningMessage', { datasourceType }) + ); + } + ); }); }); diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/abap-on-prem/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/abap-on-prem/questions.test.ts index d09c89b3ed..557487804a 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/abap-on-prem/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/abap-on-prem/questions.test.ts @@ -18,9 +18,9 @@ import LoggerHelper from '../../../../../src/prompts/logger-helper'; import { PromptState } from '../../../../../src/utils'; import * as utils from '../../../../../src/utils'; import { hostEnvironment, promptNames } from '../../../../../src/types'; -import type { ServiceAnswer } from '../../../../../src/prompts/datasources/sap-system/new-system/types'; import { newSystemPromptNames } from '../../../../../src/prompts/datasources/sap-system/new-system/types'; import type { ChoiceOptions } from 'inquirer'; +import { type ServiceAnswer } from '../../../../../src/prompts/datasources/sap-system/service-selection/types'; const v2Metadata = ''; @@ -50,7 +50,8 @@ const connectionValidatorMock = { validateAuth: validateAuthMock, isAuthRequired: isAuthRequiredMock, serviceProvider: serviceProviderMock, - catalogs + catalogs, + systemAuthType: 'basic' }; jest.mock('../../../../../src/prompts/connectionValidator', () => { return { @@ -123,7 +124,7 @@ describe('questions', () => { "mandatory": true, }, "message": "Username", - "name": "abapSystemUsername", + "name": "abapOnPrem:systemUsername", "type": "input", "validate": [Function], "when": [Function], @@ -136,7 +137,7 @@ describe('questions', () => { "guiType": "login", "mask": "*", "message": "Password", - "name": "abapSystemPassword", + "name": "abapOnPrem:systemPassword", "type": "password", "validate": [Function], "when": [Function], @@ -215,10 +216,11 @@ describe('questions', () => { authRequired: true, reachable: true }; + connectionValidatorMock.systemAuthType = 'basic'; connectionValidatorMock.isAuthRequired = jest.fn().mockResolvedValue(true); const newSystemQuestions = getAbapOnPremQuestions(); - const userNamePrompt = newSystemQuestions.find((question) => question.name === 'abapSystemUsername'); - const passwordPrompt = newSystemQuestions.find((question) => question.name === 'abapSystemPassword'); + const userNamePrompt = newSystemQuestions.find((question) => question.name === 'abapOnPrem:systemUsername'); + const passwordPrompt = newSystemQuestions.find((question) => question.name === 'abapOnPrem:systemPassword'); expect(await (userNamePrompt?.when as Function)()).toBe(true); expect(await (passwordPrompt?.when as Function)()).toBe(true); @@ -252,11 +254,13 @@ describe('questions', () => { authRequired: true, reachable: true }; + const abapOnPremSystemUsername = 'abapOnPrem:systemUsername'; + const abapOnPremSystemPassword = 'abapOnPrem:systemPassword'; connectionValidatorMock.validateAuth = jest.fn().mockResolvedValue(true); const newSystemQuestions = getAbapOnPremQuestions(); - const userNamePrompt = newSystemQuestions.find((question) => question.name === 'abapSystemUsername'); - const passwordPrompt = newSystemQuestions.find((question) => question.name === 'abapSystemPassword'); + const userNamePrompt = newSystemQuestions.find((question) => question.name === abapOnPremSystemUsername); + const passwordPrompt = newSystemQuestions.find((question) => question.name === abapOnPremSystemPassword); // Prompt state should not be updated with the connected system until the connection is validated expect(PromptState.odataService.connectedSystem?.serviceProvider).toBe(undefined); @@ -271,13 +275,19 @@ describe('questions', () => { const abapSystemUsername = 'user01'; const password = 'pword01'; - expect(await (passwordPrompt?.validate as Function)('', { abapSystemUsername })).toBe(false); - expect(await (passwordPrompt?.validate as Function)(password, { abapSystemUsername: '' })).toBe(false); - expect(await (passwordPrompt?.validate as Function)(password, { abapSystemUsername })).toBe(true); + expect( + await (passwordPrompt?.validate as Function)('', { [abapOnPremSystemUsername]: abapSystemUsername }) + ).toBe(false); + expect(await (passwordPrompt?.validate as Function)(password, { [abapOnPremSystemUsername]: '' })).toBe(false); + expect( + await (passwordPrompt?.validate as Function)(password, { [abapOnPremSystemUsername]: abapSystemUsername }) + ).toBe(true); // Should have attempted to validate since required above conditions are met expect(connectionValidatorMock.validateAuth).toHaveBeenCalled(); - expect(await (passwordPrompt?.validate as Function)('pword01', { systemUrl, abapSystemUsername })).toBe(true); + expect( + await (passwordPrompt?.validate as Function)('pword01', { [abapOnPremSystemUsername]: abapSystemUsername }) + ).toBe(true); expect(connectionValidatorMock.validateAuth).toHaveBeenCalledWith(systemUrl, abapSystemUsername, password, { isSystem: true, sapClient: undefined diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/new-system/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/new-system/questions.test.ts index 3ba404c6de..18a4f24834 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/new-system/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/new-system/questions.test.ts @@ -82,7 +82,8 @@ describe('Test new system prompt', () => { serviceKeys: undefined, url: 'http://abap.on.prem:1234', userDisplayName: undefined, - username: 'user01' + username: 'user01', + newOrUpdated: true }); }); @@ -128,7 +129,8 @@ describe('Test new system prompt', () => { serviceKeys: undefined, url: 'http://mock.abap.on.prem:4300', userDisplayName: undefined, - username: 'testUser' + username: 'testUser', + newOrUpdated: true }); }); }); diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/questions.test.ts index cd0d1564bd..1d2aac4ecb 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/questions.test.ts @@ -54,7 +54,7 @@ describe('questions', () => { "mandatory": true, }, "message": "Username", - "name": "abapSystemUsername", + "name": "abapOnPrem:systemUsername", "type": "input", "validate": [Function], "when": [Function], @@ -67,7 +67,7 @@ describe('questions', () => { "guiType": "login", "mask": "*", "message": "Password", - "name": "abapSystemPassword", + "name": "abapOnPrem:systemPassword", "type": "password", "validate": [Function], "when": [Function], diff --git a/packages/odata-service-inquirer/tsconfig.json b/packages/odata-service-inquirer/tsconfig.json index 9ddb70d945..753e0cd27f 100644 --- a/packages/odata-service-inquirer/tsconfig.json +++ b/packages/odata-service-inquirer/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "include": [ - "src", + "src/**/*.ts", "src/**/*.json" ], "compilerOptions": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 183faf3e5b..507490f838 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1921,9 +1921,6 @@ importers: jest-extended: specifier: 3.2.4 version: 3.2.4(jest@29.7.0) - lodash: - specifier: 4.17.21 - version: 4.17.21 packages/odata-service-writer: dependencies: