Skip to content

Commit ff7fc25

Browse files
authored
fix: [BAS][Deploy] Password field in the deploy page disappears when the user edits the username. (#4934)
* fix: * Add dedicated flag which determines whether the login form should be displayed. The previously used flag has other meaning and is misused. * Chnage the behaviour of getOrCreateServiceProvider(). In case the method is called with credentials we always get a fresh provider instance not a cached one. * Fix an issue with the getSystemConfig - the backendTarget is always ignoired since the PromptState.abapDeployConfig is always an object. We return the config in the promot state only if it is a valid one otherwise we return the provided backednTarget. * fix: * When we select xyz_noauth -> xyz_basic -> xyz_noauth then no credentials prompt appear when the xyz is selected without auth for the second time. This is because both xyz_basic and xyz_noauth represent the same system and use the cached provider for the auth system (xyz_auth). The comparator used to determine the equality of two systems is wrong it disregards the destination. * Fix chageset summary. * fix: Parametrize multiple tests into a single one.
1 parent 8a7e844 commit ff7fc25

10 files changed

Lines changed: 245 additions & 81 deletions

File tree

.changeset/short-beers-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/abap-deploy-config-inquirer': patch
3+
---
4+
5+
fix: [BAS][Deploy] Password field in the deploy page disappears when the user edits the username.

packages/abap-deploy-config-inquirer/src/prompts/conditions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ export async function showUsernameQuestion(backendTarget?: BackendTarget): Promi
115115
PromptState.transportAnswers.transportConfig = transportConfig;
116116
PromptState.transportAnswers.transportConfigNeedsCreds = transportConfigNeedsCreds ?? false;
117117

118+
// Track that credential fields should remain visible for the duration of the auth flow.
119+
// This flag is NOT reset by validateCredentials, unlike transportConfigNeedsCreds which
120+
// must become false after successful auth so downstream questions (package, transport) appear.
121+
PromptState.transportAnswers.areCredentialFieldsVisible = transportConfigNeedsCreds ?? false;
122+
118123
// Provide context to the CLI when username credentials are required
119124
if (transportConfigNeedsCreds) {
120125
LoggerHelper.logger.info(t('errors.atoUnauthorisedSystem'));
@@ -128,7 +133,7 @@ export async function showUsernameQuestion(backendTarget?: BackendTarget): Promi
128133
* @returns boolean
129134
*/
130135
export function showPasswordQuestion(): boolean {
131-
return Boolean(PromptState.transportAnswers.transportConfigNeedsCreds);
136+
return !!PromptState.transportAnswers.areCredentialFieldsVisible;
132137
}
133138

134139
/**

packages/abap-deploy-config-inquirer/src/service-provider-utils/abap-service-provider.ts

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
import type { AbapServiceProvider, AxiosRequestConfig, ProviderConfiguration } from '@sap-ux/axios-extension';
12
import { isAppStudio } from '@sap-ux/btp-utils';
2-
import { isSameSystem } from '../utils.js';
3-
import { createAbapServiceProvider } from '@sap-ux/system-access';
3+
import { setGlobalRejectUnauthorized } from '@sap-ux/nodejs-utils';
44
import { AuthenticationType } from '@sap-ux/store';
5-
import { PromptState } from '../prompts/prompt-state.js';
6-
import LoggerHelper from '../logger-helper.js';
7-
import type { AbapServiceProvider, AxiosRequestConfig, ProviderConfiguration } from '@sap-ux/axios-extension';
85
import type { DestinationAbapTarget, UrlAbapTarget } from '@sap-ux/system-access';
9-
import type { BackendTarget, Credentials, SystemConfig } from '../types.js';
6+
import { createAbapServiceProvider } from '@sap-ux/system-access';
107
import type { AbapTarget } from '@sap-ux/ui5-config';
11-
import { setGlobalRejectUnauthorized } from '@sap-ux/nodejs-utils';
128
import { t } from '../i18n.js';
9+
import LoggerHelper from '../logger-helper.js';
10+
import { PromptState } from '../prompts/prompt-state.js';
11+
import { areSystemConfigEquals, isValidSystemConfig } from '../system-utils.js';
12+
import type { BackendTarget, Credentials, SystemConfig } from '../types.js';
1313

1414
/**
1515
* Class to manage the ABAP service provider used during prompting.
@@ -38,25 +38,36 @@ export class AbapServiceProviderManager {
3838
ignoreCertErrors = true;
3939
setGlobalRejectUnauthorized(false);
4040
}
41-
// 1. Use existing service provider
42-
if (this.isExistingServiceProviderValid(backendTarget)) {
41+
42+
const isExistingProviderValid = this.isExistingServiceProviderValid(backendTarget);
43+
if (isExistingProviderValid && !this.areCredentialsProvided(credentials)) {
4344
return this.abapServiceProvider as AbapServiceProvider;
4445
}
4546

46-
// 2. Use connected service provider passed in prompt options with backend target
47-
if (this.isBackendTargetServiceProviderValid(backendTarget)) {
48-
this.abapServiceProvider = backendTarget?.serviceProvider as AbapServiceProvider;
49-
await this.setIsDefaultAbapCloud();
50-
51-
return this.abapServiceProvider;
47+
if (!isExistingProviderValid && this.isBackendTargetServiceProviderValid(backendTarget)) {
48+
this.abapServiceProvider = backendTarget!.serviceProvider as AbapServiceProvider;
49+
} else {
50+
this.abapServiceProvider = await this.createNewServiceProvider(
51+
credentials,
52+
backendTarget,
53+
ignoreCertErrors
54+
);
5255
}
53-
// 3. Create a new service provider
54-
this.abapServiceProvider = await this.createNewServiceProvider(credentials, backendTarget, ignoreCertErrors);
55-
await this.setIsDefaultAbapCloud();
5656

57+
await this.setIsDefaultAbapCloud();
5758
return this.abapServiceProvider;
5859
}
5960

61+
/**
62+
* Checks if valid credentials (both username and password) are provided.
63+
*
64+
* @param credentials - user credentials
65+
* @returns true if both username and password are non-empty
66+
*/
67+
private static areCredentialsProvided(credentials?: Credentials): boolean {
68+
return !!credentials?.username && !!credentials?.password;
69+
}
70+
6071
/**
6172
* Checks if the service provider has a valid connection.
6273
*
@@ -73,12 +84,12 @@ export class AbapServiceProviderManager {
7384
* @returns - system config
7485
*/
7586
private static getSystemConfig(backendTarget?: BackendTarget): SystemConfig {
76-
const { url, client, destination } = PromptState.abapDeployConfig ?? backendTarget?.abapTarget ?? {};
77-
return {
78-
url,
79-
client,
80-
destination
81-
};
87+
// PromptState.abapDeployConfig is always an object but could lack url and destination both,
88+
// in that case we use the backendTarget.
89+
const { url, destination, client } = isValidSystemConfig(PromptState.abapDeployConfig)
90+
? PromptState.abapDeployConfig
91+
: (backendTarget?.abapTarget ?? {});
92+
return { url, destination, client };
8293
}
8394

8495
/**
@@ -89,10 +100,7 @@ export class AbapServiceProviderManager {
89100
*/
90101
private static isExistingServiceProviderValid(backendTarget?: BackendTarget): boolean {
91102
const systemConfig = this.getSystemConfig(backendTarget);
92-
if (
93-
this.abapServiceProvider &&
94-
isSameSystem(systemConfig, this.system?.url, this.system?.client, this.system?.destination)
95-
) {
103+
if (this.abapServiceProvider && areSystemConfigEquals(systemConfig, this.system)) {
96104
this.system = systemConfig;
97105
return true;
98106
}
@@ -110,17 +118,8 @@ export class AbapServiceProviderManager {
110118
private static isBackendTargetServiceProviderValid(backendTarget?: BackendTarget): boolean {
111119
if (
112120
backendTarget?.serviceProvider &&
113-
(isSameSystem(
114-
{
115-
url: PromptState.abapDeployConfig.url,
116-
client: PromptState.abapDeployConfig.client,
117-
destination: PromptState.abapDeployConfig.destination
118-
},
119-
backendTarget?.abapTarget.url,
120-
backendTarget?.abapTarget.client,
121-
backendTarget?.abapTarget.destination
122-
) ||
123-
(!PromptState.abapDeployConfig.url && !PromptState.abapDeployConfig.destination))
121+
(areSystemConfigEquals(PromptState.abapDeployConfig, backendTarget?.abapTarget) ||
122+
!isValidSystemConfig(PromptState.abapDeployConfig))
124123
) {
125124
this.system = backendTarget?.abapTarget;
126125
return true;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { type SystemConfig } from './types.js';
2+
3+
/**
4+
* Checks whether a system configuration is valid by verifying that it has either a URL or a destination defined.
5+
*
6+
* @param config - the system configuration to validate
7+
* @returns `true` if the configuration has a URL or destination, `false` otherwise
8+
*/
9+
export function isValidSystemConfig(config?: SystemConfig): boolean {
10+
if (!config) {
11+
return false;
12+
}
13+
const { url, destination } = config;
14+
return !!url || !!destination;
15+
}
16+
17+
/**
18+
* Compares two system configurations for equality.
19+
* Two configurations are considered equal if they have the same normalized URL, client, and destination.
20+
* Returns `false` if either configuration is invalid.
21+
*
22+
* @param configA - the first system configuration
23+
* @param configB - the second system configuration
24+
* @returns `true` if both configurations are valid and equal, `false` otherwise
25+
*/
26+
export function areSystemConfigEquals(configA?: SystemConfig, configB?: SystemConfig): boolean {
27+
if (!isValidSystemConfig(configA) || !isValidSystemConfig(configB)) {
28+
return false;
29+
}
30+
31+
return (
32+
normalizeSystemUrl(configA?.url) === normalizeSystemUrl(configB?.url) &&
33+
configA?.client === configB?.client &&
34+
configA?.destination === configB?.destination
35+
);
36+
}
37+
38+
/**
39+
* Normalizes a system URL by trimming whitespace and removing a trailing slash.
40+
*
41+
* @param url - the URL to normalize
42+
* @returns the normalized URL, or `undefined` if no URL was provided
43+
*/
44+
function normalizeSystemUrl(url?: string): string | undefined {
45+
return url?.trim().replace(/\/$/, '');
46+
}

packages/abap-deploy-config-inquirer/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ export interface TransportAnswers {
228228
transportConfig?: TransportConfig;
229229
transportConfigError?: string;
230230
transportConfigNeedsCreds?: boolean;
231+
/**
232+
* Tracks that credential fields (username/password) should remain visible
233+
* throughout the authentication flow.
234+
*/
235+
areCredentialFieldsVisible?: boolean;
231236
transportList?: TransportListItem[];
232237
newTransportNumber?: string;
233238
}

packages/abap-deploy-config-inquirer/src/utils.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,6 @@ export function findBackendSystemByUrl(backendUrl: string): BackendSystem | unde
7272
return cachedBackendSystems?.find((backend: BackendSystem) => backend.url === backendUrl);
7373
}
7474

75-
/**
76-
* Check if the current system is the same as the one in the answers.
77-
*
78-
* @param abapSystem - system configuration
79-
* @param url - url
80-
* @param client - client
81-
* @param destination - destination
82-
* @returns true if the system is the same
83-
*/
84-
export function isSameSystem(abapSystem?: SystemConfig, url?: string, client?: string, destination?: string): boolean {
85-
return Boolean(
86-
(abapSystem?.url &&
87-
abapSystem.url.trim()?.replace(/\/$/, '') === url?.trim()?.replace(/\/$/, '') &&
88-
abapSystem.client === client) ||
89-
(!!abapSystem?.destination && destination === abapSystem?.destination)
90-
);
91-
}
92-
9375
/**
9476
* Get transport configuration from the backend.
9577
*

packages/abap-deploy-config-inquirer/test/prompts/conditions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ describe('Test abap deploy config inquirer conditions', () => {
189189
});
190190

191191
test('should show password questions', () => {
192-
PromptState.transportAnswers.transportConfigNeedsCreds = true;
192+
PromptState.transportAnswers.areCredentialFieldsVisible = true;
193193
expect(showPasswordQuestion()).toBe(true);
194194
});
195195

packages/abap-deploy-config-inquirer/test/service-provider-utils/abap-service-provider.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('getOrCreateServiceProvider', () => {
3232

3333
afterEach(() => {
3434
AbapServiceProviderManager.deleteExistingServiceProvider();
35+
mockCreateAbapServiceProvider.mockReset();
3536
});
3637

3738
beforeEach(() => {
@@ -70,8 +71,8 @@ describe('getOrCreateServiceProvider', () => {
7071
LoggerHelper.logger
7172
);
7273

73-
// use existing provider when called again
74-
const serviceProvider2 = await AbapServiceProviderManager.getOrCreateServiceProvider(undefined, credentials);
74+
// use existing provider when called again without credentials.
75+
const serviceProvider2 = await AbapServiceProviderManager.getOrCreateServiceProvider(undefined);
7576
expect(serviceProvider2).toBe(serviceProvider);
7677
});
7778

@@ -148,4 +149,48 @@ describe('getOrCreateServiceProvider', () => {
148149
);
149150
expect(buildRequestOptionsSpy).toHaveBeenCalledWith(undefined, true);
150151
});
152+
153+
it('should create a new service provider when called again with different credentials on the same system', async () => {
154+
const abapServiceProviderA = new AbapServiceProvider();
155+
const abapServiceProviderB = new AbapServiceProvider();
156+
mockIsAppStudio.mockReturnValue(false);
157+
mockCreateAbapServiceProvider.mockResolvedValueOnce(abapServiceProviderA);
158+
mockCreateAbapServiceProvider.mockResolvedValueOnce(abapServiceProviderB);
159+
160+
PromptState.abapDeployConfig = {
161+
url: 'http://target.url',
162+
client: '100',
163+
scp: false
164+
};
165+
166+
const credentials1 = { username: 'userA', password: 'passwordA' };
167+
const credentials2 = { username: 'userB', password: 'passwordB' };
168+
169+
const serviceProvider1 = await AbapServiceProviderManager.getOrCreateServiceProvider(undefined, credentials1);
170+
expect(serviceProvider1).toBe(abapServiceProviderA);
171+
172+
const serviceProvider2 = await AbapServiceProviderManager.getOrCreateServiceProvider(undefined, credentials2);
173+
expect(serviceProvider2).toBe(abapServiceProviderB);
174+
expect(serviceProvider2).not.toBe(serviceProvider1);
175+
expect(mockCreateAbapServiceProvider).toHaveBeenCalledTimes(2);
176+
});
177+
178+
it('should return cached service provider when called without credentials after initial creation', async () => {
179+
const abapServiceProvider = new AbapServiceProvider();
180+
mockIsAppStudio.mockReturnValue(false);
181+
mockCreateAbapServiceProvider.mockResolvedValueOnce(abapServiceProvider);
182+
183+
PromptState.abapDeployConfig = {
184+
url: 'http://target.url',
185+
client: '100',
186+
scp: false
187+
};
188+
189+
const serviceProviderA = await AbapServiceProviderManager.getOrCreateServiceProvider();
190+
expect(serviceProviderA).toBe(abapServiceProvider);
191+
192+
const serviceProviderB = await AbapServiceProviderManager.getOrCreateServiceProvider();
193+
expect(serviceProviderB).toBe(serviceProviderA);
194+
expect(mockCreateAbapServiceProvider).toHaveBeenCalledTimes(1);
195+
});
151196
});

0 commit comments

Comments
 (0)