diff --git a/packages/fiori-app-sub-generator/src/fiori-app-generator/end.ts b/packages/fiori-app-sub-generator/src/fiori-app-generator/end.ts index 4e4ac53e3e8..06b3a646426 100644 --- a/packages/fiori-app-sub-generator/src/fiori-app-generator/end.ts +++ b/packages/fiori-app-sub-generator/src/fiori-app-generator/end.ts @@ -100,6 +100,7 @@ export async function runPostGenerationTasks( service: { backendSystem?: BackendSystem & { newOrUpdated?: boolean; + temporaryCredentials?: boolean; }; capService?: CapService; sapClient?: string; @@ -141,7 +142,12 @@ export async function runPostGenerationTasks( // Persist backend system connection information const hostEnv = getHostEnvironment(); - if (service.backendSystem && hostEnv !== hostEnvironment.bas && service.backendSystem.newOrUpdated) { + if ( + service.backendSystem && + hostEnv !== hostEnvironment.bas && + service.backendSystem.newOrUpdated && + !service.backendSystem.temporaryCredentials + ) { const storeService = await getService({ logger: logger, entityName: 'system' @@ -149,6 +155,12 @@ export async function runPostGenerationTasks( // No need to await, we cannot recover anyway // eslint-disable-next-line @typescript-eslint/no-floating-promises storeService.write(service.backendSystem, { force: true }); + } else if ( + service.backendSystem?.newOrUpdated === false && + hostEnv !== hostEnvironment.bas && + service.backendSystem.temporaryCredentials + ) { + logger.info(t('logMessages.noCredentialsBackendSystem', { system: service.backendSystem.name })); } // Display info message if using a cap service as it is not otherwise shown when a top level dir is not created diff --git a/packages/fiori-app-sub-generator/src/translations/fioriAppSubGenerator.i18n.json b/packages/fiori-app-sub-generator/src/translations/fioriAppSubGenerator.i18n.json index cd25d204f76..6d631f9da82 100644 --- a/packages/fiori-app-sub-generator/src/translations/fioriAppSubGenerator.i18n.json +++ b/packages/fiori-app-sub-generator/src/translations/fioriAppSubGenerator.i18n.json @@ -89,7 +89,8 @@ "warningCachingNotSupported": "Warning: caching is not supported.", "attemptingToExecutePostGenerationCommand": "Attempting to execute command after app generation: {{- command}}", "installSkippedOptionSpecified": "Option `--skipInstall` was specified. Installation of dependencies will be skipped.", - "generatorExiting": "Application generation exiting due to error: {{error}}" + "generatorExiting": "Application generation was cancelled due to the error: {{error}}.", + "noCredentialsBackendSystem": "No credentials were found for the back-end system: {{system}}. Skipping storing credentials." }, "error": { "fatalError": "An error has occurred, please restart the generator.", diff --git a/packages/fiori-app-sub-generator/test/unit/fiori-app-generator/end.test.ts b/packages/fiori-app-sub-generator/test/unit/fiori-app-generator/end.test.ts index 4f0b136671b..8fa3af27915 100644 --- a/packages/fiori-app-sub-generator/test/unit/fiori-app-generator/end.test.ts +++ b/packages/fiori-app-sub-generator/test/unit/fiori-app-generator/end.test.ts @@ -111,6 +111,55 @@ describe('runPostGenerationTasks', () => { expect(storeServiceWriteMock).toHaveBeenCalledWith(service.backendSystem, { force: true }); }); + it('should NOT persist backend system when newOrUpdated is false', async () => { + const service = { + backendSystem: { + newOrUpdated: false + } as unknown as BackendSystem, + sapClient: '100', + odataVersion: OdataVersion.v2, + datasourceType: DatasourceType.sapSystem + }; + const project = { + targetFolder: '/path/to/project', + name: 'testProject', + flpAppId: 'testAppId' + }; + + (getHostEnvironment as jest.Mock).mockReturnValue(hostEnvironment.vscode); + + await runPostGenerationTasks({ service, project }, fs, logger, vscode, appWizard); + + // Should not call getService or write when newOrUpdated is false + expect(getService).not.toHaveBeenCalled(); + expect(storeServiceWriteMock).not.toHaveBeenCalled(); + }); + + it('should NOT persist backend system with temporary credentials', async () => { + const service = { + backendSystem: { + newOrUpdated: false, + temporaryCredentials: true + } as unknown as BackendSystem, + sapClient: '100', + odataVersion: OdataVersion.v2, + datasourceType: DatasourceType.sapSystem + }; + const project = { + targetFolder: '/path/to/project', + name: 'testProject', + flpAppId: 'testAppId' + }; + + (getHostEnvironment as jest.Mock).mockReturnValue(hostEnvironment.vscode); + + await runPostGenerationTasks({ service, project }, fs, logger, vscode, appWizard); + + // Should not call getService or write when temporaryCredentials is true + expect(getService).not.toHaveBeenCalled(); + expect(storeServiceWriteMock).not.toHaveBeenCalled(); + }); + it('should show information message for cap projects', async () => { const service = { capService: {} as unknown as CapService, 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 index 9a4d2f5fa90..f02131f74e3 100644 --- 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 @@ -9,7 +9,7 @@ import { promptNames } from '../../../../types'; import { PromptState, removeCircularFromServiceProvider } from '../../../../utils'; import type { ConnectionValidator } from '../../../connectionValidator'; import type { ValidationResult } from '../../../types'; -import type { SystemSelectionAnswerType } from '../system-selection/prompt-helpers'; +import type { SystemSelectionAnswerType, BackendSystemSelection } from '../system-selection/prompt-helpers'; import type { NewSystemAnswers } from '../new-system/types'; export enum BasicCredentialsPromptNames { @@ -53,7 +53,15 @@ export function getCredentialsPrompts( guiOptions: { mandatory: true }, - default: '', + default: (answers: T) => { + // Prefill username from the selected backend system if available + const selectedSystem = answers?.[promptNames.systemSelection] as SystemSelectionAnswerType; + if (selectedSystem?.type === 'backendSystem') { + const backendSystem = selectedSystem.system as BackendSystemSelection; + return backendSystem.username || ''; + } + return ''; + }, validate: (user: string) => user?.length > 0 } as InputQuestion, { @@ -112,6 +120,13 @@ export function getCredentialsPrompts( return valResult; }, additionalMessages: (password: string, answers: T) => { + if (PromptState.odataService.connectedSystem?.backendSystem?.newOrUpdated) { + return { + message: t('texts.passwordStoreWarning'), + severity: Severity.information + }; + } + // Since the odata service URL prompt has its own credentials prompts its safe to assume // that `ignoreCertError` when true means that the user has set the node setting to ignore cert errors and // not that the user has chosen to ignore the cert error for this specific connection (this is only supported by the odata service URL prompts). @@ -149,14 +164,21 @@ function updatePromptStateWithConnectedSystem( }; // Update the existing backend system with the new credentials that may be used to update in the store. if (selectedSystem?.type === 'backendSystem') { - const backendSystem = selectedSystem.system as BackendSystem; + const backendSystem = selectedSystem.system as BackendSystemSelection; + // Have the credentials changed.. if (backendSystem.username !== username || backendSystem.password !== password) { + // Determine if this is a temporary credentials scenario or an update scenario + const isTemporaryCredentials = !backendSystem.hasStoredCredentials; + PromptState.odataService.connectedSystem.backendSystem = Object.assign(backendSystem, { username: username, password, userDisplayName: username, - newOrUpdated: true + // Only set newOrUpdated to true if the system had existing credentials (auth error scenario) + // For systems without credentials, set temporaryCredentials flag instead + newOrUpdated: !isTemporaryCredentials, + temporaryCredentials: isTemporaryCredentials } as Partial); } // If the connection is successful and a destination was selected, assign the connected destination to the prompt state. 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 index 83431a77b80..7b10511a38e 100644 --- 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 @@ -10,7 +10,7 @@ import { } from '@sap-ux/btp-utils'; import { ERROR_TYPE } from '@sap-ux/inquirer-common'; import type { OdataVersion } from '@sap-ux/odata-service-writer'; -import { type BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store'; +import { BackendSystemKey, type BackendSystem, SystemService } from '@sap-ux/store'; import type { ListChoiceOptions } from 'inquirer'; import { t } from '../../../../i18n'; import type { ConnectedSystem, DestinationFilters } from '../../../../types'; @@ -27,9 +27,14 @@ export type NewSystemChoice = typeof NewSystemChoice; export const CfAbapEnvServiceChoice = 'cfAbapEnvService'; export type CfAbapEnvServiceChoice = typeof CfAbapEnvServiceChoice; +// Local extension of BackendSystem to include credential information +export interface BackendSystemSelection extends BackendSystem { + hasStoredCredentials?: boolean; +} + export type SystemSelectionAnswerType = { type: 'destination' | 'backendSystem' | 'newSystemChoice' | CfAbapEnvServiceChoice; - system: Destination | BackendSystem | NewSystemChoice | CfAbapEnvServiceChoice; + system: Destination | BackendSystemSelection | NewSystemChoice | CfAbapEnvServiceChoice; }; /** @@ -72,6 +77,9 @@ export async function connectWithBackendSystem( convertODataVersionType(requiredOdataVersion) ); } else if (backendSystem.authenticationType === 'basic' || !backendSystem.authenticationType) { + // Check if the system has stored credentials + const hasStoredCredentials = !!(backendSystem.username && backendSystem.password); + let errorType; ({ valResult: connectValResult, errorType } = await connectionValidator.validateAuth( backendSystem.url, @@ -85,17 +93,18 @@ export async function connectWithBackendSystem( )); // If authentication failed with existing credentials the user will be prompted to enter new credentials. // We log the error in case there is another issue (unresolveable) with the stored backend configuration. - if ( - errorType === ERROR_TYPE.AUTH && - typeof backendSystem.username === 'string' && - typeof backendSystem.password === 'string' - ) { + if (errorType === ERROR_TYPE.AUTH && hasStoredCredentials) { LoggerHelper.logger.error( t('errors.storedSystemConnectionError', { systemName: backendSystem.name, error: connectValResult }) ); + (backendSystem as BackendSystemSelection).hasStoredCredentials = true; + return true; + } else if (errorType === ERROR_TYPE.AUTH && !hasStoredCredentials) { + // 1f there are no stored credentials, we defer validation to the credentials prompt but do not log an error here. + (backendSystem as BackendSystemSelection).hasStoredCredentials = false; return true; } } @@ -242,15 +251,36 @@ export async function createSystemChoices( // Cache the backend systems PromptState.backendSystemsCache = backendSystems; - systemChoices = backendSystems.map((system) => { - return { - name: getBackendSystemDisplayName(system), - value: { - system, - type: 'backendSystem' - } as SystemSelectionAnswerType - }; - }); + systemChoices = await Promise.all( + backendSystems.map(async (system) => { + // Check if the system has stored credentials by reading it with sensitive data + const systemWithCredentials = await new SystemService(LoggerHelper.logger).read( + BackendSystemKey.from(system) as BackendSystemKey + ); + const hasStoredCredentials = + typeof systemWithCredentials?.username === 'string' && + systemWithCredentials.username.length > 0 && + typeof systemWithCredentials?.password === 'string' && + systemWithCredentials.password.length > 0; + let systemSelection: BackendSystemSelection = { ...system }; + + if (system.authenticationType === 'basic' || !system.authenticationType) { + systemSelection = { + ...systemSelection, + hasStoredCredentials, + username: systemWithCredentials?.username || system.username + }; + } + + return { + name: getBackendSystemDisplayName(system), + value: { + system: systemSelection, + type: 'backendSystem' + } as SystemSelectionAnswerType + }; + }) + ); newSystemChoice = { name: t('prompts.systemSelection.newSystemChoiceLabel'), value: { type: 'newSystemChoice', system: NewSystemChoice } as SystemSelectionAnswerType 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 index f7c7fd2bcd6..5da4d8126d4 100644 --- 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 @@ -20,6 +20,7 @@ import { getSystemServiceQuestion } from '../service-selection/questions'; import { validateServiceUrl } from '../validators'; import { type SystemSelectionAnswerType, + type BackendSystemSelection, connectWithBackendSystem, connectWithDestination, createSystemChoices, @@ -209,23 +210,35 @@ export async function getSystemConnectionQuestions( ); }, additionalMessages: async (selectedSystem: SystemSelectionAnswerType) => { - // Backend systems credentials may need to be updated + let message; + // Check for stored credentials or authentication failure message if a backend system is selected if ( selectedSystem.type === 'backendSystem' && connectionValidator.systemAuthType === 'basic' && (await connectionValidator.isAuthRequired()) ) { - return { - message: t('prompts.systemSelection.authenticationFailedUpdateCredentials'), - severity: Severity.information - }; + const backend = selectedSystem.system as BackendSystemSelection; + const missingCredentials = !backend.hasStoredCredentials; + + if (missingCredentials) { + message = { + message: t('prompts.systemSelection.noStoredCredentials'), + severity: Severity.information + }; + } else { + message = { + message: t('prompts.systemSelection.authenticationFailedUpdateCredentials'), + severity: Severity.information + }; + } } if (connectionValidator.ignoreCertError) { - return { + message = { message: t('warnings.certErrorIgnoredByNodeSetting'), severity: Severity.warning }; } + return message ? message : undefined; } } as ListQuestion ]; 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 99cdb87e2fc..797ec12a34f 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 @@ -107,7 +107,8 @@ "newSystemChoiceLabel": "New System", "hint": "Select a system configuration.", "message": "System", - "authenticationFailedUpdateCredentials": "Authentication failed. Please try updating the credentials." + "authenticationFailedUpdateCredentials": "Authentication failed. Check your credentials are correct and try again.", + "noStoredCredentials": "This stored system has no credentials. Please provide them. They will not be saved with system details." }, "abapOnBTPType": { "message": "ABAP environment definition source", @@ -256,6 +257,7 @@ "forUserName": "(for user [{{username}}])", "httpStatus": "HTTP Status {{httpStatus}}", "checkDestinationAuthConfig": "Please check the SAP BTP destination authentication configuration.", - "choiceNameNone": "None" + "choiceNameNone": "None", + "passwordStoreWarning": "Passwords are stored in your operating system's credential manager and are protected by its security policies." } } 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 index 66292fc7ab7..c9ced089f7e 100644 --- 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 @@ -43,9 +43,11 @@ exports[`API tests getPrompts, i18n is loaded 1`] = ` "name": "storedSystem1", "value": { "system": { + "hasStoredCredentials": true, "name": "storedSystem1", "systemType": "OnPrem", "url": "http://url1", + "username": "user1", }, "type": "backendSystem", }, @@ -54,9 +56,11 @@ exports[`API tests getPrompts, i18n is loaded 1`] = ` "name": "storedSystem2 (ABAP Cloud)", "value": { "system": { + "hasStoredCredentials": false, "name": "storedSystem2", "systemType": "BTP", "url": "http://url2", + "username": undefined, }, "type": "backendSystem", }, @@ -75,7 +79,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = ` "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, @@ -156,7 +160,7 @@ exports[`API tests getPrompts, i18n is loaded 1`] = ` "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, 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 097c6ddfad0..150bf0c5724 100644 --- a/packages/odata-service-inquirer/test/unit/index-api.test.ts +++ b/packages/odata-service-inquirer/test/unit/index-api.test.ts @@ -33,7 +33,25 @@ jest.mock('@sap-ux/store', () => ({ url: 'http://url2', systemType: 'BTP' } - ] as BackendSystem[]) + ] as BackendSystem[]), + read: jest.fn().mockImplementation((key) => { + // Mock read to return systems with credentials + const systems = [ + { + name: 'storedSystem1', + url: 'http://url1', + systemType: 'OnPrem', + username: 'user1', + password: 'pass1' + }, + { + name: 'storedSystem2', + url: 'http://url2', + systemType: 'BTP' + } + ]; + return Promise.resolve(systems.find((s) => s.url === key.url)); + }) })) })); 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 index 39ec0be62fd..46cdbd1ba78 100644 --- 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 @@ -43,9 +43,11 @@ exports[`getQuestions getQuestions 1`] = ` "name": "storedSystem1", "value": { "system": { + "hasStoredCredentials": true, "name": "storedSystem1", "systemType": "OnPrem", "url": "http://url1", + "username": "user1", }, "type": "backendSystem", }, @@ -54,9 +56,11 @@ exports[`getQuestions getQuestions 1`] = ` "name": "storedSystem2 (ABAP Cloud)", "value": { "system": { + "hasStoredCredentials": false, "name": "storedSystem2", "systemType": "BTP", "url": "http://url2", + "username": undefined, }, "type": "backendSystem", }, @@ -79,7 +83,7 @@ exports[`getQuestions getQuestions 1`] = ` "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, @@ -164,7 +168,7 @@ exports[`getQuestions getQuestions 1`] = ` "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, 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 990fe19719c..4154159b1cd 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/prompts.test.ts @@ -31,7 +31,25 @@ jest.mock('@sap-ux/store', () => ({ url: 'http://url2', systemType: 'BTP' } - ] as BackendSystem[]) + ] as BackendSystem[]), + read: jest.fn().mockImplementation((key) => { + // Mock read to return systems with credentials + const systems = [ + { + name: 'storedSystem1', + url: 'http://url1', + systemType: 'OnPrem', + username: 'user1', + password: 'pass1' + }, + { + name: 'storedSystem2', + url: 'http://url2', + systemType: 'BTP' + } + ]; + return Promise.resolve(systems.find((s) => s.url === key.url)); + }) })) })); 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 504a34092e7..a8e614afc0c 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 @@ -82,7 +82,7 @@ describe('questions', () => { "validate": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/credentials/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/credentials/questions.test.ts index 955724998f7..2d3ac867872 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/credentials/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/credentials/questions.test.ts @@ -94,6 +94,82 @@ describe('Test credentials prompts', () => { expect(await (passwordPrompt?.when as Function)()).toBe(true); }); + test('should prefill username from selected backend system if available', async () => { + const connectionValidator = new ConnectionValidator(); + connectionValidatorMock.validity = { + authenticated: false, + authRequired: true, + reachable: true + }; + connectionValidatorMock.systemAuthType = 'basic'; + connectionValidatorMock.isAuthRequired = jest.fn().mockResolvedValue(true); + const credentialsPrompts = getCredentialsPrompts(connectionValidator, promptNamespace); + const userNamePrompt = credentialsPrompts.find( + (question) => question.name === systemUsernamePromptName + ) as InputQuestion; + + const backendSystemWithUsername: BackendSystem = { + name: 'http://abap.on.prem:1234', + url: 'http://abap.on.prem:1234', + username: 'user1', + password: 'password1', + client: '001' + }; + + const answersWithBackendSystem = { + [promptNames.systemSelection]: { + type: 'backendSystem', + system: backendSystemWithUsername + } + }; + + expect(userNamePrompt.default?.(answersWithBackendSystem as Record)).toBe( + backendSystemWithUsername.username + ); + + const backendSystemWithoutUsername: BackendSystem = { + name: 'http://abap.on.prem:1234', + url: 'http://abap.on.prem:1234', + client: '001' + }; + + const answersWithoutBackendSystem = { + [promptNames.systemSelection]: { + type: 'backendSystem', + system: backendSystemWithoutUsername + } + }; + + expect(userNamePrompt.default?.(answersWithoutBackendSystem as Record)).toBe(''); + }); + + test('should not prefill username if selected system is not a backend system or username is not available', async () => { + const connectionValidator = new ConnectionValidator(); + connectionValidatorMock.validity = { + authenticated: false, + authRequired: true, + reachable: true + }; + connectionValidatorMock.systemAuthType = 'basic'; + connectionValidatorMock.isAuthRequired = jest.fn().mockResolvedValue(true); + const credentialsPrompts = getCredentialsPrompts(connectionValidator, promptNamespace); + const userNamePrompt = credentialsPrompts.find( + (question) => question.name === systemUsernamePromptName + ) as InputQuestion; + + const answersWithDestination = { + [promptNames.systemSelection]: { + type: 'destination', + system: { + Name: 'dest1', + Host: 'http://dest1.com' + } as Destination + } + }; + + expect(userNamePrompt.default?.(answersWithDestination as Record)).toBe(''); + }); + test('should validate username/password using ConnectionValidator', async () => { const connectionValidator = new ConnectionValidator(); connectionValidatorMock.validity = { @@ -269,4 +345,46 @@ describe('Test credentials prompts', () => { } as NewSystemAnswers) ).toBeUndefined(); }); + + test('should show additional store information warning about operating system credential storage policies', async () => { + const connectionValidator = new ConnectionValidator(); + connectionValidatorMock.validity = { + authenticated: false, + authRequired: true, + reachable: true + }; + connectionValidatorMock.systemAuthType = 'basic'; + connectionValidatorMock.validatedUrl = 'http://abap01:1234'; + connectionValidatorMock.ignoreCertError = false; + + const credentialsPrompts = getCredentialsPrompts(connectionValidator, promptNamespace); + const passwordPrompt = credentialsPrompts.find( + (question) => question.name === systemPasswordPromptName + ) as PasswordQuestion; + + // Set up a connected system with newOrUpdated flag + PromptState.odataService.connectedSystem = { + serviceProvider: serviceProviderMock as ServiceProvider, + backendSystem: { + name: 'test-system', + url: 'http://test.system:1234', + username: 'testuser', + password: 'testpass', + client: '001', + newOrUpdated: true + } + }; + + // Should show password store warning when system is new or updated + expect(passwordPrompt.additionalMessages?.('123')).toEqual({ + message: t('texts.passwordStoreWarning'), + severity: Severity.information + }); + + // Should not show message when system is not marked as new/updated + if (PromptState.odataService.connectedSystem?.backendSystem) { + PromptState.odataService.connectedSystem.backendSystem.newOrUpdated = false; + } + expect(passwordPrompt.additionalMessages?.('123')).toBeUndefined(); + }); }); 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 5ae8d29e527..68591d008b0 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 @@ -55,7 +55,7 @@ describe('questions', () => { "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/__snapshots__/questions.test.ts.snap b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/__snapshots__/questions.test.ts.snap index f1995904c34..883717d38bd 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/__snapshots__/questions.test.ts.snap +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/__snapshots__/questions.test.ts.snap @@ -42,7 +42,7 @@ exports[`Test system selection prompts should return system selection prompts an "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, @@ -131,6 +131,7 @@ exports[`Test system selection prompts should return system selection prompts an "name": "http://abap.on.prem:1234", "value": { "system": { + "hasStoredCredentials": false, "name": "http://abap.on.prem:1234", "password": "password1", "systemType": "OnPrem", @@ -153,7 +154,7 @@ exports[`Test system selection prompts should return system selection prompts an "validate": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, @@ -234,7 +235,7 @@ exports[`Test system selection prompts should return system selection prompts an "when": [Function], }, { - "default": "", + "default": [Function], "guiOptions": { "mandatory": true, }, diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/prompt-helpers.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/prompt-helpers.test.ts index bba21be3745..bddd5518faa 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/prompt-helpers.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/prompt-helpers.test.ts @@ -1,5 +1,6 @@ import { listDestinations } from '@sap-ux/btp-utils'; import { initI18nOdataServiceInquirer } from '../../../../../src/i18n'; +import type { BackendSystemSelection } from '../../../../../src/prompts/datasources/sap-system/system-selection/prompt-helpers'; import { CfAbapEnvServiceChoice, createSystemChoices, @@ -10,12 +11,13 @@ import type { AuthenticationType, BackendSystem } from '@sap-ux/store'; import type { Destination, Destinations } from '@sap-ux/btp-utils'; import type { AxiosError } from '@sap-ux/axios-extension'; -const backendSystemBasic: BackendSystem = { +const backendSystemBasic: BackendSystemSelection = { name: 'http://abap.on.prem:1234', url: 'http://abap.on.prem:1234', username: 'user1', password: 'password1', - systemType: 'OnPrem' + systemType: 'OnPrem', + hasStoredCredentials: true }; const backendSystemReentrance: BackendSystem = { @@ -44,6 +46,11 @@ jest.mock('@sap-ux/store', () => ({ // Mock store access SystemService: jest.fn().mockImplementation(() => ({ getAll: jest.fn().mockResolvedValueOnce(backendSystems), + read: jest.fn().mockImplementation((key) => { + // Mock read to return systems with credentials + const system = backendSystems.find((s) => s.url === key.url); + return Promise.resolve(system); + }), partialUpdate: jest.fn().mockImplementation((system: BackendSystem) => { return Promise.resolve(system); }) diff --git a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts index aa58b2fc5ea..8ed7894aeb2 100644 --- a/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts +++ b/packages/odata-service-inquirer/test/unit/prompts/sap-system/system-selection/questions.test.ts @@ -2,6 +2,7 @@ import type { AbapServiceProvider, ServiceProvider, V2CatalogService, V4CatalogS import { ODataVersion } from '@sap-ux/axios-extension'; import type { Destination, Destinations } from '@sap-ux/btp-utils'; import { WebIDEAdditionalData, WebIDEUsage } from '@sap-ux/btp-utils'; +import { Severity } from '@sap-devx/yeoman-ui-types'; import type { ListQuestion } from '@sap-ux/inquirer-common'; import { hostEnvironment } from '@sap-ux/fiori-generator-shared'; import type { SystemService, BackendSystem } from '@sap-ux/store'; @@ -23,7 +24,6 @@ import LoggerHelper from '../../../../../src/prompts/logger-helper'; import type { ConnectedSystem } from '../../../../../src/types'; import { promptNames } from '../../../../../src/types'; import { getPromptHostEnvironment, PromptState } from '../../../../../src/utils'; -import { isFeatureEnabled } from '@sap-ux/feature-toggle'; jest.mock('../../../../../src/utils', () => ({ ...jest.requireActual('../../../../../src/utils'), @@ -37,6 +37,13 @@ const backendSystemBasic: BackendSystem = { password: 'password1', systemType: 'OnPrem' }; + +const backendSystemBasicNoCreds: promptHelpers.BackendSystemSelection = { + name: 'http://abap.on.prem:1234', + url: 'http://abap.on.prem:1234', + username: 'user1', + hasStoredCredentials: false +}; const backendSystemReentrance: BackendSystem = { name: 'http://s4hc:1234', url: 'http:/s4hc:1234', @@ -416,7 +423,7 @@ describe('Test system selection prompts', () => { } as SystemSelectionAnswerType) ).toMatchInlineSnapshot(` { - "message": "Authentication failed. Please try updating the credentials.", + "message": "Authentication failed. Check your credentials are correct and try again.", "severity": 2, } `); @@ -430,6 +437,29 @@ describe('Test system selection prompts', () => { expect(await (passwordPrompt?.when as Function)()).toBe(true); }); + test('getSystemConnectionQuestions: non-BAS (BackendSystem, AuthType: basic) - no stored credentials', async () => { + // If no stored credentials and auth is required, auth fails but hasStoredCredentials is set to false - message: t('prompts.systemSelection.noStoredCredentials'), + mockIsAppStudio = false; + const connectValidator = new ConnectionValidator(); + + // Setup the connection validator mock to simulate the required conditions + connectionValidatorMock.systemAuthType = 'basic'; + isAuthRequiredMock.mockResolvedValue(true); + + const systemConnectionQuestions = await getSystemConnectionQuestions(connectValidator); + const systemSelectionPrompt = systemConnectionQuestions[0] as ListQuestion; + + const result = await systemSelectionPrompt.additionalMessages?.({ + type: 'backendSystem', + system: backendSystemBasicNoCreds + } as SystemSelectionAnswerType); + + expect(result).toEqual({ + message: t('prompts.systemSelection.noStoredCredentials'), + severity: Severity.information + }); + }); + test('getSystemConnectionQuestions: non-BAS (BackendSystem, AuthType: reentranceTicket)', async () => { mockIsAppStudio = false; const connectValidator = new ConnectionValidator();