sap-ai-dev-toolkit 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/abap-developer.agent.md +11 -7
- package/.github/agents/abap-runtime-debugger.agent.md +3 -1
- package/.github/agents/rap-service-developer.agent.md +12 -8
- package/.github/agents/sap-solution-architect.agent.md +5 -3
- package/.github/skills/abap-development/SKILL.md +6 -4
- package/.github/skills/rap-development/SKILL.md +6 -4
- package/.github/skills/rap-service-delivery/SKILL.md +7 -5
- package/.github/skills/sap-sdlc-orchestration/SKILL.md +5 -4
- package/README.md +27 -8
- package/package.json +2 -1
- package/scripts/install-local.mjs +115 -0
- package/src/bas-destination-relay.mjs +282 -0
- package/src/bas-discovery.mjs +4 -0
- package/src/credential-overrides.mjs +75 -0
- package/src/credentials-store.mjs +123 -0
- package/src/launcher.mjs +24 -4
- package/src/mcp-proxy.mjs +120 -32
- package/src/setup.mjs +115 -2
- package/src/terminal-ui.mjs +84 -0
- package/test/bas-destination-relay.test.mjs +238 -0
- package/test/cf-runtime.test.mjs +27 -5
- package/test/copilot-content.test.mjs +56 -0
- package/test/credentials-store.test.mjs +187 -0
- package/test/discovery.test.mjs +1 -1
- package/test/launcher.test.mjs +52 -0
- package/test/mcp-proxy.test.mjs +59 -3
- package/test/setup-cf.test.mjs +131 -4
package/test/launcher.test.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { spawnWithPty } from './pty.mjs';
|
|
10
|
+
import { storeDestinationCredentials } from '../src/credentials-store.mjs';
|
|
11
|
+
import { credentialKeyForDestination, enrichWithStoredCredentials } from '../src/credential-overrides.mjs';
|
|
10
12
|
|
|
11
13
|
const launcher = fileURLToPath(new URL('../src/launcher.mjs', import.meta.url));
|
|
12
14
|
const fakeVsp = fileURLToPath(new URL('./fixtures/fake-vsp.mjs', import.meta.url));
|
|
@@ -43,6 +45,56 @@ function runLauncherTty(env, input, args = ['--setup']) {
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
test('enriches stored credentials without switching BAS OnPremise or CF Connectivity routes', async t => {
|
|
49
|
+
const directory = await mkdtemp(join(tmpdir(), 'bas-launcher-credentials-'));
|
|
50
|
+
const credentialsPath = join(directory, 'credentials.json');
|
|
51
|
+
t.after(async () => { await rm(directory, { recursive: true, force: true }); });
|
|
52
|
+
|
|
53
|
+
const internet = {
|
|
54
|
+
name: 'internet-system', url: 'http://internet-system.dest', backendUrl: 'https://internet.example:44300',
|
|
55
|
+
authentication: 'BasicAuthentication', proxyType: 'Internet'
|
|
56
|
+
};
|
|
57
|
+
const basOnPremise = {
|
|
58
|
+
name: 'shared-system', url: 'http://shared-system.dest', backendUrl: 'https://internal.example:44300',
|
|
59
|
+
authentication: 'BasicAuthentication', proxyType: 'OnPremise'
|
|
60
|
+
};
|
|
61
|
+
const cfOnPremise = {
|
|
62
|
+
source: 'cloud-foundry', name: 'shared-system', serverName: 'cf:space-one:instance-one:shared-system',
|
|
63
|
+
url: 'https://sap.internal:44300', authentication: 'BasicAuthentication', proxyType: 'OnPremise',
|
|
64
|
+
childEnv: { HTTP_PROXY: 'http://127.0.0.1:3333', SAP_USER: 'configured-user', SAP_PASSWORD: 'configured-password' }
|
|
65
|
+
};
|
|
66
|
+
const principalPropagation = { ...cfOnPremise, name: 'principal-system', serverName: 'cf:space-one:instance-one:principal-system', authentication: 'PrincipalPropagation' };
|
|
67
|
+
|
|
68
|
+
await storeDestinationCredentials(credentialsPath, 'internet-system', {
|
|
69
|
+
host: internet.backendUrl, user: 'INTERNET_USER', password: 'internet-secret'
|
|
70
|
+
});
|
|
71
|
+
await storeDestinationCredentials(credentialsPath, 'shared-system', {
|
|
72
|
+
user: 'BAS_USER', password: 'bas-secret', mode: 'bas-tunnel'
|
|
73
|
+
});
|
|
74
|
+
await storeDestinationCredentials(credentialsPath, credentialKeyForDestination(cfOnPremise), {
|
|
75
|
+
user: 'CF_USER', password: 'cf-secret', mode: 'cf-connectivity'
|
|
76
|
+
});
|
|
77
|
+
await storeDestinationCredentials(credentialsPath, credentialKeyForDestination(principalPropagation), {
|
|
78
|
+
user: 'SHOULD_NOT_APPLY', password: 'pp-secret', mode: 'cf-connectivity'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await enrichWithStoredCredentials(
|
|
82
|
+
[internet, basOnPremise, cfOnPremise, principalPropagation],
|
|
83
|
+
{ SAP_AI_DEV_TOOLKIT_CREDENTIALS_FILE: credentialsPath },
|
|
84
|
+
() => {}
|
|
85
|
+
);
|
|
86
|
+
assert.deepEqual(result[0].credentials, {
|
|
87
|
+
host: internet.backendUrl, user: 'INTERNET_USER', password: 'internet-secret', mode: 'direct'
|
|
88
|
+
});
|
|
89
|
+
assert.deepEqual(result[1].credentials, { user: 'BAS_USER', password: 'bas-secret', mode: 'bas-tunnel' });
|
|
90
|
+
assert.equal(result[1].credentials.host, undefined, 'OnPremise must not bypass the BAS destination route');
|
|
91
|
+
assert.deepEqual(result[2].childEnv, {
|
|
92
|
+
HTTP_PROXY: 'http://127.0.0.1:3333', SAP_USER: 'CF_USER', SAP_PASSWORD: 'cf-secret', SAP_VERBOSE: 'false'
|
|
93
|
+
});
|
|
94
|
+
assert.equal(result[3].childEnv.SAP_USER, 'configured-user', 'PrincipalPropagation must not receive a Basic override');
|
|
95
|
+
assert.equal(result[3].childEnv.SAP_PASSWORD, 'configured-password');
|
|
96
|
+
});
|
|
97
|
+
|
|
46
98
|
test('help exits before BAS destination discovery', async t => {
|
|
47
99
|
let requests = 0;
|
|
48
100
|
const server = createServer((_request, response) => {
|
package/test/mcp-proxy.test.mjs
CHANGED
|
@@ -47,6 +47,57 @@ test('uses authentication-specific arguments for Cloud Foundry children', () =>
|
|
|
47
47
|
assert.equal(childArguments(principal, {}).includes('--proxy-auth'), true);
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
+
test('self-heals BAS destinations through a local destination relay without credentials', async t => {
|
|
51
|
+
const destination = { name: 'S4H', url: 'http://S4H.dest', client: '100', authentication: 'BasicAuthentication', proxyType: 'Internet' };
|
|
52
|
+
const directory = await mkdtemp(join(tmpdir(), 'bas-relay-'));
|
|
53
|
+
const log = join(directory, 'children.log');
|
|
54
|
+
const logs = [];
|
|
55
|
+
const proxy = new MCPProxy({ binary: fixture, destinations: [destination], env: { ...process.env, FAKE_LOG: log }, log: message => logs.push(message) });
|
|
56
|
+
t.after(async () => { await proxy.close(); await rm(directory, { recursive: true, force: true }); });
|
|
57
|
+
proxy.start();
|
|
58
|
+
await proxy.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
59
|
+
const event = (await readFile(log, 'utf8')).trim().split('\n').map(line => JSON.parse(line)).find(row => row.event === 'initialize');
|
|
60
|
+
assert.equal(event.argv.includes('--proxy-auth'), true);
|
|
61
|
+
assert.match(event.argv[event.argv.indexOf('--url') + 1], /^http:\/\/127\.0\.0\.1:\d+$/);
|
|
62
|
+
assert.equal(event.env.user, undefined);
|
|
63
|
+
assert.equal(event.env.password, undefined);
|
|
64
|
+
assert.ok(logs.some(message => message.includes('BAS destination relay enabled')));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('self-heals BAS relay children with loopback NO_PROXY and keeps --proxy-auth', async t => {
|
|
68
|
+
const destination = { name: 'S4H', url: 'http://S4H.dest', client: '100', authentication: 'BasicAuthentication', proxyType: 'Internet' };
|
|
69
|
+
const directory = await mkdtemp(join(tmpdir(), 'bas-relay-env-'));
|
|
70
|
+
const log = join(directory, 'children.log');
|
|
71
|
+
const proxy = new MCPProxy({ binary: fixture, destinations: [destination], env: { ...process.env, FAKE_LOG: log }, log: () => {} });
|
|
72
|
+
t.after(async () => { await proxy.close(); await rm(directory, { recursive: true, force: true }); });
|
|
73
|
+
proxy.start();
|
|
74
|
+
await proxy.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} });
|
|
75
|
+
const event = (await readFile(log, 'utf8')).trim().split('\n').map(line => JSON.parse(line)).find(row => row.event === 'initialize');
|
|
76
|
+
assert.equal(event.argv.includes('--proxy-auth'), true);
|
|
77
|
+
const noProxy = String(event.env.noProxy || '').split(',').map(value => value.trim().toLowerCase());
|
|
78
|
+
assert.ok(noProxy.includes('127.0.0.1'), `NO_PROXY must exempt the relay loopback: ${event.env.noProxy}`);
|
|
79
|
+
assert.ok(noProxy.includes('localhost'), `NO_PROXY must exempt relay localhost: ${event.env.noProxy}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('restarts a crashed VSP child and retries the tool call once', async t => {
|
|
83
|
+
const { directory, log, proxy } = await fixtureProxy();
|
|
84
|
+
t.after(async () => { await proxy.close(); await rm(directory, { recursive: true, force: true }); });
|
|
85
|
+
await proxy.handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05' } });
|
|
86
|
+
await proxy.handle({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
|
|
87
|
+
const entry = proxy.children[0];
|
|
88
|
+
const originalChild = entry.child;
|
|
89
|
+
// Kill the child behind the proxy's back to simulate a crash.
|
|
90
|
+
const exit = once(originalChild.process, 'exit');
|
|
91
|
+
originalChild.process.kill('SIGKILL');
|
|
92
|
+
await exit;
|
|
93
|
+
const response = await proxy.handle({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'alpha__GetSystemInfo', arguments: {} } });
|
|
94
|
+
assert.equal(response?.error, undefined, `self-healing must recover the call: ${JSON.stringify(response?.error)}`);
|
|
95
|
+
assert.notEqual(proxy.children[0].child, originalChild, 'a replacement child must have been spawned');
|
|
96
|
+
const events = (await readFile(log, 'utf8')).trim().split('\n').map(line => JSON.parse(line));
|
|
97
|
+
assert.ok(events.some(row => row.event === 'initialize' && row.destination === 'alpha'), 'replacement child must re-initialize');
|
|
98
|
+
assert.ok(events.some(row => row.event === 'call' && row.name === 'GetSystemInfo'), 'the retried call must reach the child');
|
|
99
|
+
});
|
|
100
|
+
|
|
50
101
|
test('closes each Cloud Foundry route after child exit and on startup failure', async t => {
|
|
51
102
|
const directory = await mkdtemp(join(tmpdir(), 'bas-cf-child-lifecycle-'));
|
|
52
103
|
const log = join(directory, 'children.log');
|
|
@@ -89,7 +140,8 @@ test('closes each Cloud Foundry route after child exit and on startup failure',
|
|
|
89
140
|
spawn: () => { throw new Error('spawn denied'); },
|
|
90
141
|
log: () => {}
|
|
91
142
|
});
|
|
92
|
-
|
|
143
|
+
failedProxy.start();
|
|
144
|
+
await assert.rejects(() => failedProxy.starting, /No destination child could be started/);
|
|
93
145
|
await new Promise(resolve => setImmediate(resolve));
|
|
94
146
|
assert.equal(failedStartCloseCount, 1);
|
|
95
147
|
});
|
|
@@ -302,9 +354,13 @@ test('returns JSON-RPC errors for unknown namespaces and logs child failures', a
|
|
|
302
354
|
const child = proxy.children.find(entry => entry.destination.name === 'alpha').child;
|
|
303
355
|
child.process.kill('SIGKILL');
|
|
304
356
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
305
|
-
|
|
306
|
-
|
|
357
|
+
// A crashed child no longer fails the call: self-healing restarts it and
|
|
358
|
+
// retries once, so the request goes through to a fresh child.
|
|
359
|
+
const healed = await proxy.handle({ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'alpha__GetSource' } });
|
|
360
|
+
assert.equal(healed?.error, undefined, `call must heal after child crash: ${JSON.stringify(healed?.error)}`);
|
|
361
|
+
assert.equal(healed.result.content[0].text, 'alpha:GetSource');
|
|
307
362
|
assert.ok(logs.some(message => message.includes('alpha') && message.includes('tools/call GetSource failed')));
|
|
363
|
+
assert.ok(logs.some(message => message.includes('self-healing restart complete')), 'restart must be logged');
|
|
308
364
|
});
|
|
309
365
|
test('stdin EOF shuts down every child process and forwards child logs', async t => {
|
|
310
366
|
const { directory, log, proxy } = await fixtureProxy();
|
package/test/setup-cf.test.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { installMcpConfig } from '../src/mcp-config.mjs';
|
|
8
8
|
import { destinationTable } from '../scripts/postinstall.mjs';
|
|
9
|
+
import { readCredentials, resolveCredentialsPath, storeDestinationCredentials } from '../src/credentials-store.mjs';
|
|
10
|
+
import { credentialKeyForDestination } from '../src/credential-overrides.mjs';
|
|
9
11
|
import { spawnWithPty } from './pty.mjs';
|
|
10
12
|
|
|
11
13
|
const setupModule = fileURLToPath(new URL('../src/setup.mjs', import.meta.url));
|
|
@@ -28,6 +30,22 @@ function cloudFoundryDestination({ name, serverName, instanceGuid, instanceName,
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function cloudFoundryOnPremiseDestination() {
|
|
34
|
+
const destination = cloudFoundryDestination({
|
|
35
|
+
name: 'shared-name',
|
|
36
|
+
serverName: 'cf:space-one:cf-destination:shared-name',
|
|
37
|
+
instanceGuid: 'cf-destination',
|
|
38
|
+
instanceName: 'destination-service',
|
|
39
|
+
keyName: 'destination-key'
|
|
40
|
+
});
|
|
41
|
+
destination.authentication = 'BasicAuthentication';
|
|
42
|
+
destination.proxyType = 'OnPremise';
|
|
43
|
+
destination.cf.connectivityInstanceGuid = 'cf-connectivity';
|
|
44
|
+
destination.cf.connectivityInstanceName = 'connectivity-service';
|
|
45
|
+
destination.cf.connectivityKeyName = 'connectivity-key';
|
|
46
|
+
return destination;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
async function makeFixture(t, { version = 'cf version 8.18.0', space = 'space-one', deleteMode = 'success' } = {}) {
|
|
32
50
|
const directory = await mkdtemp(join(tmpdir(), 'bas-cf-setup-'));
|
|
33
51
|
const bin = join(directory, 'bin');
|
|
@@ -63,6 +81,7 @@ await runSetup({
|
|
|
63
81
|
await writeFile(process.env.CF_DISCOVERY_LOG, JSON.stringify({ spaceGuid, managedKeys }));
|
|
64
82
|
return JSON.parse(process.env.TEST_CF_RESULT);
|
|
65
83
|
},
|
|
84
|
+
verifyOnPremCredentialRoute: async () => JSON.parse(process.env.TEST_ONPREM_CSRF_RESULT),
|
|
66
85
|
...(process.env.TEST_INSTALL_FAIL === 'true' ? {
|
|
67
86
|
install: async () => { throw new Error('simulated config write failure'); }
|
|
68
87
|
} : {})
|
|
@@ -81,13 +100,14 @@ await runSetup({
|
|
|
81
100
|
TEST_CF_DELETE_MODE: deleteMode,
|
|
82
101
|
TEST_INSTALL_FAIL: 'false',
|
|
83
102
|
TEST_BAS_DESTINATIONS: JSON.stringify([]),
|
|
84
|
-
TEST_CF_RESULT: JSON.stringify({ destinations: [], createdKeys: [], warnings: [] })
|
|
103
|
+
TEST_CF_RESULT: JSON.stringify({ destinations: [], createdKeys: [], warnings: [] }),
|
|
104
|
+
TEST_ONPREM_CSRF_RESULT: JSON.stringify({ httpStatus: 200, tokenReceived: true, cookieCount: 1 })
|
|
85
105
|
};
|
|
86
106
|
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
87
107
|
return { directory, configPath, callLog, discoveryLog, harness, env };
|
|
88
108
|
}
|
|
89
109
|
|
|
90
|
-
function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r' } = {}) {
|
|
110
|
+
function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r', promptAnswers = [] } = {}) {
|
|
91
111
|
return new Promise((resolve, reject) => {
|
|
92
112
|
const command = `${JSON.stringify(process.execPath)} ${JSON.stringify(fixture.harness)}`;
|
|
93
113
|
const child = spawnWithPty(command, { env: fixture.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -95,6 +115,7 @@ function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r' } = {}
|
|
|
95
115
|
let stderr = '';
|
|
96
116
|
let importSent = false;
|
|
97
117
|
let selectionSent = false;
|
|
118
|
+
let promptAnswerIndex = 0;
|
|
98
119
|
const timeout = setTimeout(() => child.kill('SIGKILL'), 15000);
|
|
99
120
|
child.stdout.on('data', chunk => {
|
|
100
121
|
stdout += chunk.toString();
|
|
@@ -104,7 +125,16 @@ function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r' } = {}
|
|
|
104
125
|
}
|
|
105
126
|
if (!selectionSent && stdout.includes('Select destinations')) {
|
|
106
127
|
selectionSent = true;
|
|
107
|
-
child.stdin.
|
|
128
|
+
if (promptAnswers.length) child.stdin.write(selection);
|
|
129
|
+
else child.stdin.end(selection);
|
|
130
|
+
}
|
|
131
|
+
if (selectionSent && promptAnswerIndex < promptAnswers.length) {
|
|
132
|
+
const answer = promptAnswers[promptAnswerIndex];
|
|
133
|
+
if (stdout.includes(answer.when)) {
|
|
134
|
+
promptAnswerIndex++;
|
|
135
|
+
child.stdin.write(answer.value);
|
|
136
|
+
if (promptAnswerIndex === promptAnswers.length) child.stdin.end();
|
|
137
|
+
}
|
|
108
138
|
}
|
|
109
139
|
});
|
|
110
140
|
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
|
|
@@ -114,7 +144,7 @@ function runSetupInPty(fixture, { importAnswer = 'y\r', selection = ' \r' } = {}
|
|
|
114
144
|
});
|
|
115
145
|
child.on('exit', (code, signal) => {
|
|
116
146
|
clearTimeout(timeout);
|
|
117
|
-
resolve({ code, signal, stdout, stderr, importSent, selectionSent });
|
|
147
|
+
resolve({ code, signal, stdout, stderr, importSent, selectionSent, promptAnswersSent: promptAnswerIndex });
|
|
118
148
|
});
|
|
119
149
|
});
|
|
120
150
|
}
|
|
@@ -145,6 +175,7 @@ test('imports selected CF destinations, passes managed key references, and remov
|
|
|
145
175
|
assert.equal(result.selectionSent, true, result.stdout);
|
|
146
176
|
assert.match(result.stdout, /CF new-destination-service/);
|
|
147
177
|
assert.match(result.stdout, /Configured 1 MCP server/);
|
|
178
|
+
assert.match(result.stdout, /GitHub Copilot Chat.*Chat tools picker.*enable each selected destination server/s);
|
|
148
179
|
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /SECRET|clientSecret|Password/);
|
|
149
180
|
|
|
150
181
|
const snapshot = JSON.parse(await readFile(fixture.discoveryLog, 'utf8'));
|
|
@@ -163,6 +194,102 @@ test('imports selected CF destinations, passes managed key references, and remov
|
|
|
163
194
|
assert.equal(calls.some(args => args.at(-1) === 'new-key'), false);
|
|
164
195
|
});
|
|
165
196
|
|
|
197
|
+
test('prompts for BAS and CF OnPremise overrides separately and keeps their routes distinct', async t => {
|
|
198
|
+
const fixture = await makeFixture(t);
|
|
199
|
+
const cfDestination = cloudFoundryOnPremiseDestination();
|
|
200
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([
|
|
201
|
+
{
|
|
202
|
+
name: 'internet-system', url: 'http://internet-system.dest', backendUrl: 'https://internet.example:44300',
|
|
203
|
+
client: '100', authentication: 'BasicAuthentication', proxyType: 'Internet',
|
|
204
|
+
probe: { status: 'available', available: true }
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'shared-name', url: 'http://shared-name.dest', backendUrl: 'https://internal.example:44300',
|
|
208
|
+
client: '101', authentication: 'BasicAuthentication', proxyType: 'OnPremise',
|
|
209
|
+
probe: { status: 'available', available: true }
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'principal-system', url: 'http://principal-system.dest',
|
|
213
|
+
client: '102', authentication: 'PrincipalPropagation', proxyType: 'OnPremise',
|
|
214
|
+
probe: { status: 'available', available: true }
|
|
215
|
+
}
|
|
216
|
+
]);
|
|
217
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({ destinations: [cfDestination], createdKeys: [], warnings: [] });
|
|
218
|
+
|
|
219
|
+
const result = await runSetupInPty(fixture, {
|
|
220
|
+
selection: 'a\r',
|
|
221
|
+
promptAnswers: [
|
|
222
|
+
{ when: 'BAS Internet — internet-system — override SAP credentials?', value: 'y\r' },
|
|
223
|
+
{ when: 'BAS Internet — internet-system — SAP user', value: 'INTERNET_USER\r' },
|
|
224
|
+
{ when: 'BAS Internet — internet-system — SAP password', value: 'internet-password-secret\r' },
|
|
225
|
+
{ when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
|
|
226
|
+
{ when: 'BAS OnPremise — shared-name — SAP user', value: 'BAS_CC_USER\r' },
|
|
227
|
+
{ when: 'BAS OnPremise — shared-name — SAP password', value: 'bas-cc-password-secret\r' },
|
|
228
|
+
{ when: 'Cloud Foundry OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
|
|
229
|
+
{ when: 'Cloud Foundry OnPremise — shared-name — SAP user', value: 'CF_CC_USER\r' },
|
|
230
|
+
{ when: 'Cloud Foundry OnPremise — shared-name — SAP password', value: 'cf-cc-password-secret\r' }
|
|
231
|
+
]
|
|
232
|
+
});
|
|
233
|
+
assert.equal(result.code, 0, `${result.stdout}\n${result.stderr}`);
|
|
234
|
+
assert.equal(result.promptAnswersSent, 9, result.stdout);
|
|
235
|
+
assert.doesNotMatch(result.stdout, /PrincipalPropagation — principal-system — override SAP credentials/);
|
|
236
|
+
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /internet-password-secret|bas-cc-password-secret|cf-cc-password-secret/);
|
|
237
|
+
|
|
238
|
+
const credentialsPath = await resolveCredentialsPath(fixture.env, fixture.configPath);
|
|
239
|
+
const stored = (await readCredentials(credentialsPath)).destinations;
|
|
240
|
+
assert.equal(stored['internet-system'].mode, 'direct');
|
|
241
|
+
assert.equal(stored['internet-system'].host, 'https://internet.example:44300');
|
|
242
|
+
assert.equal(stored['shared-name'].mode, 'bas-tunnel');
|
|
243
|
+
assert.equal(stored['shared-name'].host, undefined, 'BAS OnPremise must not store or use the backend URL');
|
|
244
|
+
const cfKey = credentialKeyForDestination(cfDestination);
|
|
245
|
+
assert.equal(stored[cfKey].mode, 'cf-connectivity');
|
|
246
|
+
assert.equal(stored[cfKey].host, undefined);
|
|
247
|
+
assert.deepEqual(
|
|
248
|
+
[stored['shared-name'].user, stored[cfKey].user],
|
|
249
|
+
['BAS_CC_USER', 'CF_CC_USER'],
|
|
250
|
+
'same destination names from BAS and CF must remain distinct'
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const mcpConfig = JSON.parse(await readFile(fixture.configPath, 'utf8'));
|
|
254
|
+
assert.equal(JSON.stringify(mcpConfig).includes('bas-cc-password-secret'), false);
|
|
255
|
+
assert.equal(JSON.stringify(mcpConfig).includes('cf-cc-password-secret'), false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('No removes a stored BAS OnPremise override and failed CSRF preflight refuses a new one', async t => {
|
|
259
|
+
const fixture = await makeFixture(t);
|
|
260
|
+
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|
|
261
|
+
name: 'shared-name', url: 'http://shared-name.dest', backendUrl: 'https://internal.example:44300',
|
|
262
|
+
client: '100', authentication: 'BasicAuthentication', proxyType: 'OnPremise',
|
|
263
|
+
probe: { status: 'available', available: true }
|
|
264
|
+
}]);
|
|
265
|
+
fixture.env.TEST_CF_RESULT = JSON.stringify({ destinations: [], createdKeys: [], warnings: [] });
|
|
266
|
+
const credentialsPath = await resolveCredentialsPath(fixture.env, fixture.configPath);
|
|
267
|
+
await storeDestinationCredentials(credentialsPath, 'shared-name', {
|
|
268
|
+
user: 'OLD_USER', password: 'old-password-secret', mode: 'bas-tunnel'
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const declined = await runSetupInPty(fixture, {
|
|
272
|
+
selection: ' \r',
|
|
273
|
+
promptAnswers: [{ when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'n\r' }]
|
|
274
|
+
});
|
|
275
|
+
assert.equal(declined.code, 0, `${declined.stdout}\n${declined.stderr}`);
|
|
276
|
+
assert.equal((await readCredentials(credentialsPath)).destinations['shared-name'], undefined);
|
|
277
|
+
|
|
278
|
+
fixture.env.TEST_ONPREM_CSRF_RESULT = JSON.stringify({ httpStatus: 200, tokenReceived: true, cookieCount: 0 });
|
|
279
|
+
const rejected = await runSetupInPty(fixture, {
|
|
280
|
+
selection: ' \r',
|
|
281
|
+
promptAnswers: [
|
|
282
|
+
{ when: 'BAS OnPremise — shared-name — override SAP credentials?', value: 'y\r' },
|
|
283
|
+
{ when: 'BAS OnPremise — shared-name — SAP user', value: 'NEW_USER\r' },
|
|
284
|
+
{ when: 'BAS OnPremise — shared-name — SAP password', value: 'new-password-secret\r' }
|
|
285
|
+
]
|
|
286
|
+
});
|
|
287
|
+
assert.equal(rejected.code, 0, `${rejected.stdout}\n${rejected.stderr}`);
|
|
288
|
+
assert.match(rejected.stdout, /did not return both a CSRF token and session cookie/);
|
|
289
|
+
assert.equal((await readCredentials(credentialsPath)).destinations['shared-name'], undefined);
|
|
290
|
+
assert.doesNotMatch(`${rejected.stdout}\n${rejected.stderr}`, /new-password-secret/);
|
|
291
|
+
});
|
|
292
|
+
|
|
166
293
|
test('declining CF import keeps BAS setup and does not create CF keys', async t => {
|
|
167
294
|
const fixture = await makeFixture(t);
|
|
168
295
|
fixture.env.TEST_BAS_DESTINATIONS = JSON.stringify([{
|