test(02-01): add failing test stubs for all four generators

- rclone-conf.test.ts: azureblob/s3/s3-compatible fixtures, error cases
- intune-install.test.ts: configPath, includeInstall, UTF-8 write, credential safety
- intune-detection.test.ts: configPath, exit codes, stdout only (no STDERR contamination)
- rmm-script.test.ts: configPath, includeInstall, ErrorActionPreference=Stop
This commit is contained in:
2026-03-26 10:53:34 +01:00
parent 0722046c32
commit 964022b5fc
4 changed files with 410 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
// src/generators/intune-detection.test.ts
// Tests for buildIntuneDetection — Intune PowerShell detection script generator.
// RED: imports will fail until Plan 02-03 creates intune-detection.ts.
import { buildIntuneDetection } from './intune-detection';
import { INITIAL_STATE } from '../store/types';
import type { WizardState } from '../store/types';
// --- Fixtures ---
const machineWideState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-azure',
backendType: 'azureblob',
params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' },
},
deployment: {
...INITIAL_STATE.deployment,
configPath: 'machine-wide',
},
};
const userProfileState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
configPath: 'user-profile',
},
};
// --- Tests ---
describe('buildIntuneDetection — config path', () => {
it('machine-wide: contains C:\\ProgramData\\rclone', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).toContain('C:\\ProgramData\\rclone');
});
it('user-profile: contains %APPDATA%\\rclone', () => {
const output = buildIntuneDetection(userProfileState);
expect(output).toContain('%APPDATA%\\rclone');
});
});
describe('buildIntuneDetection — exit codes', () => {
it('contains exit 0 (detected / compliant)', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).toContain('exit 0');
});
it('contains exit 1 (not detected / non-compliant)', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).toContain('exit 1');
});
});
describe('buildIntuneDetection — STDOUT signal', () => {
it('uses Write-Output for STDOUT signal (Intune reads stdout, not stderr)', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).toContain('Write-Output');
});
});
describe('buildIntuneDetection — no STDERR contamination', () => {
it('does NOT contain $ErrorActionPreference (avoids STDERR contamination in detection scripts)', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).not.toContain('$ErrorActionPreference');
});
it('does NOT contain Write-Error', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).not.toContain('Write-Error');
});
it('does NOT contain Write-Host', () => {
const output = buildIntuneDetection(machineWideState);
expect(output).not.toContain('Write-Host');
});
});
+96
View File
@@ -0,0 +1,96 @@
// src/generators/intune-install.test.ts
// Tests for buildIntuneInstall — Intune PowerShell install script generator.
// RED: imports will fail until Plan 02-02 creates intune-install.ts.
import { buildIntuneInstall } from './intune-install';
import { INITIAL_STATE } from '../store/types';
import type { WizardState } from '../store/types';
// --- Fixtures ---
const machineWideState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-azure',
backendType: 'azureblob',
params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' },
},
deployment: {
...INITIAL_STATE.deployment,
configPath: 'machine-wide',
includeInstall: false,
},
};
const userProfileState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
configPath: 'user-profile',
},
};
const withInstallState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
includeInstall: true,
},
};
const withoutInstallState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
includeInstall: false,
},
};
// --- Tests ---
describe('buildIntuneInstall — config path', () => {
it('machine-wide: contains C:\\ProgramData\\rclone', () => {
const output = buildIntuneInstall(machineWideState);
expect(output).toContain('C:\\ProgramData\\rclone');
});
it('user-profile: contains %APPDATA%\\rclone', () => {
const output = buildIntuneInstall(userProfileState);
expect(output).toContain('%APPDATA%\\rclone');
});
});
describe('buildIntuneInstall — rclone install download', () => {
it('includeInstall=true: output contains rclone download URL', () => {
const output = buildIntuneInstall(withInstallState);
expect(output).toContain('https://downloads.rclone.org/rclone-current-windows-amd64.zip');
});
it('includeInstall=false: output does NOT contain downloads.rclone.org', () => {
const output = buildIntuneInstall(withoutInstallState);
expect(output).not.toContain('downloads.rclone.org');
});
});
describe('buildIntuneInstall — script structure', () => {
it('uses [System.IO.File]::WriteAllText for UTF-8 no-BOM write', () => {
const output = buildIntuneInstall(machineWideState);
expect(output).toContain('[System.IO.File]::WriteAllText');
});
it('exits with exit 0 on success', () => {
const output = buildIntuneInstall(machineWideState);
expect(output).toContain('exit 0');
});
});
describe('buildIntuneInstall — credential safety', () => {
it('does NOT interpolate credential variables inside config content (no $account inside here-string)', () => {
const output = buildIntuneInstall(machineWideState);
// The config content block should contain literal values, not PS variable interpolation
// Find the WriteAllText call and verify the content arg has no $account or $key variables
// We check that there is no $account or $key inside the config block
expect(output).not.toMatch(/\$account\b/);
expect(output).not.toMatch(/\$key\b/);
});
});
+149
View File
@@ -0,0 +1,149 @@
// src/generators/rclone-conf.test.ts
// Tests for buildRcloneConf — rclone INI config generator.
// RED: imports will fail until Plan 02-02 creates rclone-conf.ts.
import { buildRcloneConf } from './rclone-conf';
import { INITIAL_STATE } from '../store/types';
import type { WizardState } from '../store/types';
// --- Fixtures ---
const azureState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-azure',
backendType: 'azureblob',
params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' },
},
};
const s3State: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-s3',
backendType: 's3',
params: {
provider: 'AWS',
access_key_id: 'AKID',
secret_access_key: 'SKEY',
region: 'us-east-1',
},
},
};
const s3CompatibleState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-wasabi',
backendType: 's3-compatible',
params: {
provider: 'Other',
access_key_id: 'WCID',
secret_access_key: 'WSKEY',
endpoint: 'https://s3.wasabisys.com',
region: '',
},
},
};
// --- Tests ---
describe('buildRcloneConf — azureblob', () => {
it('contains the remote section header', () => {
const output = buildRcloneConf(azureState);
expect(output).toContain('[my-azure]');
});
it('contains type = azureblob', () => {
const output = buildRcloneConf(azureState);
expect(output).toContain('type = azureblob');
});
it('contains account value', () => {
const output = buildRcloneConf(azureState);
expect(output).toContain('account = mystorageaccount');
});
it('contains key value', () => {
const output = buildRcloneConf(azureState);
expect(output).toContain('key = BASE64KEY');
});
it('omits sas_url when empty', () => {
const output = buildRcloneConf(azureState);
expect(output).not.toContain('sas_url');
});
});
describe('buildRcloneConf — s3', () => {
it('contains the remote section header', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('[my-s3]');
});
it('contains type = s3', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('type = s3');
});
it('contains provider = AWS', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('provider = AWS');
});
it('contains access_key_id', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('access_key_id = AKID');
});
it('contains secret_access_key', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('secret_access_key = SKEY');
});
it('contains region', () => {
const output = buildRcloneConf(s3State);
expect(output).toContain('region = us-east-1');
});
});
describe('buildRcloneConf — s3-compatible', () => {
it('uses type = s3 (not type = s3-compatible)', () => {
const output = buildRcloneConf(s3CompatibleState);
expect(output).toContain('type = s3');
expect(output).not.toContain('type = s3-compatible');
});
it('contains provider = Other', () => {
const output = buildRcloneConf(s3CompatibleState);
expect(output).toContain('provider = Other');
});
it('contains endpoint', () => {
const output = buildRcloneConf(s3CompatibleState);
expect(output).toContain('endpoint = https://s3.wasabisys.com');
});
it('omits region when empty', () => {
const output = buildRcloneConf(s3CompatibleState);
expect(output).not.toContain('region');
});
});
describe('buildRcloneConf — error cases', () => {
it('throws when backendType is null', () => {
const state: WizardState = {
...INITIAL_STATE,
remote: { name: 'test', backendType: null, params: {} },
};
expect(() => buildRcloneConf(state)).toThrow();
});
it('throws when remote name is empty string', () => {
const state: WizardState = {
...INITIAL_STATE,
remote: { name: '', backendType: 'azureblob', params: { account: 'acc', key: 'k', sas_url: '' } },
};
expect(() => buildRcloneConf(state)).toThrow();
});
});
+85
View File
@@ -0,0 +1,85 @@
// src/generators/rmm-script.test.ts
// Tests for buildRmmScript — RMM (NinjaRMM / Datto etc.) PowerShell script generator.
// RED: imports will fail until Plan 02-04 creates rmm-script.ts.
import { buildRmmScript } from './rmm-script';
import { INITIAL_STATE } from '../store/types';
import type { WizardState } from '../store/types';
// --- Fixtures ---
const machineWideState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-azure',
backendType: 'azureblob',
params: { account: 'mystorageaccount', key: 'BASE64KEY', sas_url: '' },
},
deployment: {
...INITIAL_STATE.deployment,
configPath: 'machine-wide',
includeInstall: false,
},
};
const userProfileState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
configPath: 'user-profile',
},
};
const withInstallState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
includeInstall: true,
},
};
const withoutInstallState: WizardState = {
...machineWideState,
deployment: {
...machineWideState.deployment,
includeInstall: false,
},
};
// --- Tests ---
describe('buildRmmScript — config path', () => {
it('machine-wide: contains C:\\ProgramData\\rclone', () => {
const output = buildRmmScript(machineWideState);
expect(output).toContain('C:\\ProgramData\\rclone');
});
it('user-profile: contains %APPDATA%\\rclone', () => {
const output = buildRmmScript(userProfileState);
expect(output).toContain('%APPDATA%\\rclone');
});
});
describe('buildRmmScript — rclone install download', () => {
it('includeInstall=true: output contains rclone download URL', () => {
const output = buildRmmScript(withInstallState);
expect(output).toContain('https://downloads.rclone.org/rclone-current-windows-amd64.zip');
});
it('includeInstall=false: output does NOT contain downloads.rclone.org', () => {
const output = buildRmmScript(withoutInstallState);
expect(output).not.toContain('downloads.rclone.org');
});
});
describe('buildRmmScript — script structure', () => {
it("uses \$ErrorActionPreference = 'Stop' (RMM scripts should halt on error)", () => {
const output = buildRmmScript(machineWideState);
expect(output).toContain("$ErrorActionPreference = 'Stop'");
});
it('uses [System.IO.File]::WriteAllText for UTF-8 no-BOM write', () => {
const output = buildRmmScript(machineWideState);
expect(output).toContain('[System.IO.File]::WriteAllText');
});
});