Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/soft-moons-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sap-ux/generator-adp': patch
'@sap-ux/adp-tooling': patch
---

feat: Prompt for credentials when not available in VSCode for adp generator
59 changes: 59 additions & 0 deletions packages/adp-tooling/src/base/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { getService, BackendSystem, BackendSystemKey, SystemType } from '@sap-ux/store';
import type { SystemLookup } from '../source';
import type { ToolsLogger } from '@sap-ux/logger';
import type { ConfigAnswers } from '../types';

/**
* Stores system credentials securely using the @sap-ux/store service.
* Only stores credentials for ABAP environments when all required fields are provided.
*
* @param {ConfigAnswers} configAnswers - Configuration answers containing credentials and system info
* @param {SystemLookup} systemLookup - System lookup service for retrieving endpoint details
* @param {ToolsLogger} logger - Logger for informational and warning messages
* @returns {Promise<void>} Promise that resolves when credentials are stored or operation completes
*/
export async function storeCredentials(
configAnswers: ConfigAnswers,
systemLookup: SystemLookup,
logger: ToolsLogger
): Promise<void> {
if (!configAnswers.username || !configAnswers.password) {
return;
}

try {
const systemEndpoint = await systemLookup.getSystemByName(configAnswers.system);
if (!systemEndpoint?.Url) {
logger.warn('Cannot store credentials: system endpoint or URL not found.');
return;
}

const systemService = await getService<BackendSystem, BackendSystemKey>({
entityName: 'system'
});

const backendSystemKey = new BackendSystemKey({
url: systemEndpoint.Url,
client: systemEndpoint.Client
});

const existingSystem = await systemService.read(backendSystemKey);

const backendSystem = new BackendSystem({
name: configAnswers.system,
url: systemEndpoint.Url,
client: systemEndpoint.Client,
username: configAnswers.username,
password: configAnswers.password,
systemType: (systemEndpoint.SystemType as SystemType) || SystemType.AbapOnPrem,
connectionType: 'abap_catalog'
});

await systemService.write(backendSystem, { force: !!existingSystem });

logger.info('System credentials have been stored securely.');
} catch (error) {
logger.error(`Failed to store credentials: ${error instanceof Error ? error.message : String(error)}`);
logger.debug(error);
}
}
1 change: 1 addition & 0 deletions packages/adp-tooling/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './ui5';
export * from './base/cf';
export * from './cf';
export * from './base/helper';
export * from './base/credentials';
export * from './base/constants';
export * from './base/project-builder';
export * from './base/abap/manifest-service';
Expand Down
12 changes: 9 additions & 3 deletions packages/adp-tooling/src/source/systems.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getService } from '@sap-ux/store';
import { getService, SystemType } from '@sap-ux/store';
import type { ToolsLogger } from '@sap-ux/logger';
import { isAppStudio, listDestinations } from '@sap-ux/btp-utils';
import type { BackendSystem, BackendSystemKey } from '@sap-ux/store';
Expand Down Expand Up @@ -30,6 +30,7 @@ export const transformBackendSystem = (system: BackendSystem): Endpoint => ({
UserDisplayName: system.userDisplayName,
Scp: !!system.serviceKeys,
Authentication: system.authenticationType,
SystemType: system.systemType,
Credentials: {
username: system.username,
password: system.password
Expand Down Expand Up @@ -81,7 +82,7 @@ export class SystemLookup {
entityName: 'system'
});
const backendSystems = await systemStore?.getAll();
endpoints = backendSystems.map(transformBackendSystem);
endpoints = backendSystems.filter((system) => system.name !== undefined).map(transformBackendSystem);
}
return endpoints;
} catch (e) {
Expand Down Expand Up @@ -120,7 +121,12 @@ export class SystemLookup {
if (isAppStudio()) {
return found?.Authentication === 'NoAuthentication';
} else {
return !found;
if (!found) {
return true;
}
const isOnPrem = found.SystemType === SystemType.AbapOnPrem;
const hasMissingCredentials = !found.Credentials?.username || !found.Credentials?.password;
return isOnPrem && hasMissingCredentials;
}
}
}
2 changes: 2 additions & 0 deletions packages/adp-tooling/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface ConfigAnswers {
system: string;
username: string;
password: string;
storeCredentials?: boolean;
application: SourceApplication;
fioriId?: string;
ach?: string;
Expand Down Expand Up @@ -187,6 +188,7 @@ export interface Endpoint extends Partial<Destination> {
Credentials?: { username?: string; password?: string };
UserDisplayName?: string;
Scp?: boolean;
SystemType?: string;
}

export interface ChangeInboundNavigation {
Expand Down
141 changes: 141 additions & 0 deletions packages/adp-tooling/test/unit/base/credentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { getService, SystemType } from '@sap-ux/store';
import { storeCredentials } from '../../../src';
import type { SystemLookup } from '../../../src';
import type { ToolsLogger } from '@sap-ux/logger';

jest.mock('@sap-ux/store');

describe('Credential Storage Logic', () => {
let mockSystemService: any;
let mockLogger: ToolsLogger;
let mockSystemLookup: SystemLookup;
const getServiceMock = getService as jest.Mock;

beforeEach(() => {
mockSystemService = {
read: jest.fn(),
write: jest.fn()
};

mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
} as any;

mockSystemLookup = {
getSystemByName: jest.fn()
} as any;

getServiceMock.mockResolvedValue(mockSystemService);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('storeCredentials function', () => {
it('should store credentials when credentials are provided', async () => {
const configAnswers = {
system: 'SystemA',
username: 'user1',
password: 'pass1',
application: {} as any
};

(mockSystemLookup.getSystemByName as jest.Mock).mockResolvedValue({
Name: 'SystemA',
Client: '010',
Url: 'https://example.com',
SystemType: 'OnPrem'
});

mockSystemService.read.mockResolvedValue(null);

await storeCredentials(configAnswers, mockSystemLookup, mockLogger);

expect(getServiceMock).toHaveBeenCalledWith({ entityName: 'system' });
expect(mockSystemService.read).toHaveBeenCalled();
expect(mockSystemService.write).toHaveBeenCalledWith(expect.any(Object), { force: false });
expect(mockLogger.info).toHaveBeenCalledWith('System credentials have been stored securely.');
});

it('should update existing credentials when system already exists in store', async () => {
const configAnswers = {
system: 'SystemA',
username: 'user1',
password: 'pass1',
application: {} as any
};

(mockSystemLookup.getSystemByName as jest.Mock).mockResolvedValue({
Name: 'SystemA',
Client: '010',
Url: 'https://example.com',
SystemType: 'OnPrem'
});

mockSystemService.read.mockResolvedValue({ name: 'SystemA', url: 'https://example.com' });

await storeCredentials(configAnswers, mockSystemLookup, mockLogger);

expect(mockSystemService.write).toHaveBeenCalledWith(expect.any(Object), { force: true });
expect(mockLogger.info).toHaveBeenCalledWith('System credentials have been stored securely.');
});

it('should not store credentials when password is missing', async () => {
const configAnswers = {
system: 'SystemA',
username: 'user1',
password: '',
application: {} as any
} as any;

await storeCredentials(configAnswers, mockSystemLookup, mockLogger);

expect(getServiceMock).not.toHaveBeenCalled();
expect(mockSystemService.write).not.toHaveBeenCalled();
});

it('should warn when system endpoint is not found', async () => {
const configAnswers = {
system: 'SystemA',
username: 'user1',
password: 'pass1',
application: {} as any
};

(mockSystemLookup.getSystemByName as jest.Mock).mockResolvedValue(undefined);

await storeCredentials(configAnswers, mockSystemLookup, mockLogger);

expect(mockLogger.warn).toHaveBeenCalledWith('Cannot store credentials: system endpoint or URL not found.');
expect(mockSystemService.write).not.toHaveBeenCalled();
});

it('should handle credential storage errors gracefully', async () => {
const configAnswers = {
system: 'SystemA',
username: 'user1',
password: 'pass1',
application: {} as any
};

(mockSystemLookup.getSystemByName as jest.Mock).mockResolvedValue({
Name: 'SystemA',
Client: '010',
Url: 'https://example.com',
SystemType: 'OnPrem'
});

const error = new Error('Storage failed');
mockSystemService.write.mockRejectedValue(error);

await storeCredentials(configAnswers, mockSystemLookup, mockLogger);

expect(mockLogger.error).toHaveBeenCalledWith('Failed to store credentials: Storage failed');
expect(mockLogger.debug).toHaveBeenCalled();
});
});
});
Loading
Loading