Skip to content

Commit 596602c

Browse files
author
Shubham Agarwal
committed
Make Azure CLI test doubles portable
Intercept Azure CLI child-process calls through a Node preload so the auth and metadata tests run identically without shell or executable-resolution dependencies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3e31c561-a943-4464-92f3-04ab071393c0
1 parent 9e2e389 commit 596602c

3 files changed

Lines changed: 64 additions & 92 deletions

File tree

plugins/mobile-apps/scripts/tests/auth-token-resolution.test.js

Lines changed: 11 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,72 +23,27 @@ const path = require('node:path');
2323
const { spawnSync } = require('node:child_process');
2424

2525
const HELPERS = path.join(__dirname, '..', 'lib', 'validation-helpers.js');
26+
const FAKE_AZ_PRELOAD = path.join(__dirname, 'helpers', 'fake-az-preload.js');
2627
// Connection to port 1 is refused immediately, so the challenge probe (when it
2728
// is reached at all) resolves fast and deterministically offline.
2829
const UNREACHABLE_ENV_URL = 'https://127.0.0.1:1';
2930

30-
// The fake `az` writes one line per invocation to $FAKE_AZ_LOG, then emulates
31-
// just the two subcommands getAuthToken uses:
31+
// The Node preload intercepts `execFileSync('az', ...)`, writes one line per
32+
// invocation to $FAKE_AZ_LOG, then emulates the two subcommands getAuthToken uses:
3233
// az account show --query tenantId -o tsv
3334
// az account get-access-token --resource <url> [--tenant <id>] ...
3435
// $FAKE_AZ_FAIL_TENANTS is a comma-separated list of tenants for which token
3536
// acquisition should fail (exit 1), letting a test drive the fallback chain.
36-
const FAKE_AZ = `#!/usr/bin/env node
37-
const fs = require('fs');
38-
const args = process.argv.slice(2);
39-
fs.appendFileSync(process.env.FAKE_AZ_LOG, args.join(' ') + '\\n');
40-
41-
if (args[0] === 'account' && args[1] === 'show') {
42-
process.stdout.write((process.env.FAKE_AZ_ACCOUNT_TENANT || '') + '\\n');
43-
process.exit(0);
44-
}
45-
46-
if (args[0] === 'account' && args[1] === 'get-access-token') {
47-
const tenantIndex = args.indexOf('--tenant');
48-
const tenant = tenantIndex === -1 ? '' : args[tenantIndex + 1];
49-
const failing = (process.env.FAKE_AZ_FAIL_TENANTS || '').split(',').filter(Boolean);
50-
if (tenant && failing.includes(tenant)) process.exit(1);
51-
process.stdout.write('token-for:' + (tenant || 'active-account') + '\\n');
52-
process.exit(0);
53-
}
54-
55-
process.exit(1);
56-
`;
57-
58-
function makeFakeAz(t) {
37+
function makeFakeAzLog(t) {
5938
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fake-az-'));
6039
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
61-
const scriptPath = path.join(dir, 'az.js');
62-
const azPath = path.join(dir, 'az');
63-
const azCmdPath = path.join(dir, 'az.cmd');
64-
fs.writeFileSync(scriptPath, FAKE_AZ.replace(/^#![^\n]*\n/, ''));
65-
fs.writeFileSync(
66-
azPath,
67-
`#!${process.execPath}\nrequire(${JSON.stringify(scriptPath)});\n`,
68-
{ mode: 0o755 },
69-
);
70-
fs.writeFileSync(
71-
azCmdPath,
72-
`@echo off\r\n"${process.execPath}" "${scriptPath}" %*\r\n`,
73-
);
74-
return { dir, logPath: path.join(dir, 'az.log') };
40+
return path.join(dir, 'az.log');
7541
}
7642

77-
function withPrependedPath(dir, overrides = {}) {
78-
const env = { ...process.env, ...overrides };
79-
const pathEntry = Object.entries(env).find(([key]) => key.toLowerCase() === 'path');
80-
const currentPath = pathEntry?.[1] ?? '';
81-
for (const key of Object.keys(env)) {
82-
if (key.toLowerCase() === 'path') delete env[key];
83-
}
84-
env.PATH = `${dir}${path.delimiter}${currentPath}`;
85-
return env;
86-
}
87-
88-
// Runs getAuthToken in a child process so PATH/env manipulation cannot leak
43+
// Runs getAuthToken in a child process so preload/env manipulation cannot leak
8944
// into the test runner, and returns both the token and the az invocation log.
9045
function runGetAuthToken(t, env = {}, explicitTenantId = null) {
91-
const { dir, logPath } = makeFakeAz(t);
46+
const logPath = makeFakeAzLog(t);
9247
const script = `
9348
const { getAuthToken } = require(${JSON.stringify(HELPERS)});
9449
getAuthToken(${JSON.stringify(UNREACHABLE_ENV_URL)}, ${JSON.stringify(explicitTenantId)})
@@ -98,15 +53,17 @@ function runGetAuthToken(t, env = {}, explicitTenantId = null) {
9853

9954
const result = spawnSync(process.execPath, ['-e', script], {
10055
encoding: 'utf8',
101-
env: withPrependedPath(dir, {
56+
env: {
57+
...process.env,
58+
NODE_OPTIONS: `--require=${FAKE_AZ_PRELOAD}`,
10259
FAKE_AZ_LOG: logPath,
10360
// Cleared unless a test opts in — the ambient shell may have them set.
10461
POWER_PLATFORM_TENANT_ID: '',
10562
DATAVERSE_TENANT_ID: '',
10663
FAKE_AZ_ACCOUNT_TENANT: '',
10764
FAKE_AZ_FAIL_TENANTS: '',
10865
...env,
109-
}),
66+
},
11067
});
11168

11269
assert.equal(result.status, 0, `getAuthToken failed: ${result.stderr}`);

plugins/mobile-apps/scripts/tests/dataverse-request-metadata-batch.test.js

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,25 @@ const { promisify } = require('node:util');
99

1010
const execFileAsync = promisify(execFile);
1111
const scriptPath = path.resolve(__dirname, '..', 'dataverse-request.js');
12+
const fakeAzPreload = path.join(__dirname, 'helpers', 'fake-az-preload.js');
1213

13-
function makeFakeAz(t, source) {
14+
function makeTempDir(t) {
1415
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dataverse-fake-az-'));
15-
const fakeAzScript = path.join(tempDir, 'az.js');
16-
const fakeAz = path.join(tempDir, 'az');
17-
const fakeAzCmd = path.join(tempDir, 'az.cmd');
18-
19-
fs.writeFileSync(fakeAzScript, source);
20-
fs.writeFileSync(
21-
fakeAz,
22-
`#!${process.execPath}\nrequire(${JSON.stringify(fakeAzScript)});\n`,
23-
{ mode: 0o755 },
24-
);
25-
fs.writeFileSync(
26-
fakeAzCmd,
27-
`@echo off\r\n"${process.execPath}" "${fakeAzScript}" %*\r\n`,
28-
);
2916
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }));
3017
return tempDir;
3118
}
3219

33-
function withPrependedPath(dir, overrides = {}) {
34-
const env = { ...process.env, ...overrides };
35-
const pathEntry = Object.entries(env).find(([key]) => key.toLowerCase() === 'path');
36-
const currentPath = pathEntry?.[1] ?? '';
37-
for (const key of Object.keys(env)) {
38-
if (key.toLowerCase() === 'path') delete env[key];
39-
}
40-
env.PATH = `${dir}${path.delimiter}${currentPath}`;
41-
return env;
20+
function fakeAzEnv(overrides = {}) {
21+
return {
22+
...process.env,
23+
NODE_OPTIONS: `--require=${fakeAzPreload}`,
24+
FAKE_AZ_STATIC_TOKEN: 'test-token',
25+
...overrides,
26+
};
4227
}
4328

4429
test('BATCH-METADATA reuses auth, preserves order, and stops on first failure', async (t) => {
45-
const tempDir = makeFakeAz(
46-
t,
47-
`const fs = require('node:fs');
48-
const args = process.argv.slice(2);
49-
fs.appendFileSync(process.env.FAKE_AZ_LOG, args.join(' ') + '\\n');
50-
process.stdout.write('{"accessToken":"test-token"}\\n');
51-
`,
52-
);
30+
const tempDir = makeTempDir(t);
5331
const azLog = path.join(tempDir, 'az.log');
5432

5533
const requests = [];
@@ -93,7 +71,7 @@ process.stdout.write('{"accessToken":"test-token"}\\n');
9371
'explicit-tenant',
9472
],
9573
{
96-
env: withPrependedPath(tempDir, {
74+
env: fakeAzEnv({
9775
FAKE_AZ_LOG: azLog,
9876
}),
9977
},
@@ -119,10 +97,7 @@ process.stdout.write('{"accessToken":"test-token"}\\n');
11997
});
12098

12199
test('BATCH-METADATA does not turn a post-throttle collision into success', async (t) => {
122-
const tempDir = makeFakeAz(
123-
t,
124-
`process.stdout.write('{"accessToken":"test-token"}\\n');\n`,
125-
);
100+
makeTempDir(t);
126101

127102
let requestCount = 0;
128103
const server = http.createServer((req, res) => {
@@ -151,7 +126,7 @@ test('BATCH-METADATA does not turn a post-throttle collision into success', asyn
151126
'--tenant-id',
152127
'explicit-tenant',
153128
],
154-
{ env: withPrependedPath(tempDir) },
129+
{ env: fakeAzEnv() },
155130
);
156131

157132
const output = JSON.parse(stdout);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
const childProcess = require('node:child_process');
4+
const fs = require('node:fs');
5+
6+
const originalExecFileSync = childProcess.execFileSync;
7+
8+
childProcess.execFileSync = function fakeAzExecFileSync(command, args, options) {
9+
if (command !== 'az') {
10+
return originalExecFileSync.call(this, command, args, options);
11+
}
12+
13+
const cliArgs = Array.isArray(args) ? args : [];
14+
if (process.env.FAKE_AZ_LOG) {
15+
fs.appendFileSync(process.env.FAKE_AZ_LOG, `${cliArgs.join(' ')}\n`);
16+
}
17+
18+
if (cliArgs[0] === 'account' && cliArgs[1] === 'show') {
19+
return `${process.env.FAKE_AZ_ACCOUNT_TENANT || ''}\n`;
20+
}
21+
22+
if (cliArgs[0] === 'account' && cliArgs[1] === 'get-access-token') {
23+
const tenantIndex = cliArgs.indexOf('--tenant');
24+
const tenant = tenantIndex === -1 ? '' : cliArgs[tenantIndex + 1];
25+
const failing = (process.env.FAKE_AZ_FAIL_TENANTS || '').split(',').filter(Boolean);
26+
if (tenant && failing.includes(tenant)) {
27+
const error = new Error(`Fake Azure CLI rejected tenant ${tenant}`);
28+
error.status = 1;
29+
throw error;
30+
}
31+
if (process.env.FAKE_AZ_STATIC_TOKEN) {
32+
return `${process.env.FAKE_AZ_STATIC_TOKEN}\n`;
33+
}
34+
return `token-for:${tenant || 'active-account'}\n`;
35+
}
36+
37+
const error = new Error(`Unexpected fake Azure CLI arguments: ${cliArgs.join(' ')}`);
38+
error.status = 1;
39+
throw error;
40+
};

0 commit comments

Comments
 (0)