From 964022b5fc903e3a79960928df48acd3cd6076c0 Mon Sep 17 00:00:00 2001 From: Kawa Date: Thu, 26 Mar 2026 10:53:34 +0100 Subject: [PATCH] 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 --- src/generators/intune-detection.test.ts | 80 +++++++++++++ src/generators/intune-install.test.ts | 96 +++++++++++++++ src/generators/rclone-conf.test.ts | 149 ++++++++++++++++++++++++ src/generators/rmm-script.test.ts | 85 ++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 src/generators/intune-detection.test.ts create mode 100644 src/generators/intune-install.test.ts create mode 100644 src/generators/rclone-conf.test.ts create mode 100644 src/generators/rmm-script.test.ts diff --git a/src/generators/intune-detection.test.ts b/src/generators/intune-detection.test.ts new file mode 100644 index 0000000..bf7756f --- /dev/null +++ b/src/generators/intune-detection.test.ts @@ -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'); + }); +}); diff --git a/src/generators/intune-install.test.ts b/src/generators/intune-install.test.ts new file mode 100644 index 0000000..8d61cf8 --- /dev/null +++ b/src/generators/intune-install.test.ts @@ -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/); + }); +}); diff --git a/src/generators/rclone-conf.test.ts b/src/generators/rclone-conf.test.ts new file mode 100644 index 0000000..b612b8e --- /dev/null +++ b/src/generators/rclone-conf.test.ts @@ -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(); + }); +}); diff --git a/src/generators/rmm-script.test.ts b/src/generators/rmm-script.test.ts new file mode 100644 index 0000000..5b58f9c --- /dev/null +++ b/src/generators/rmm-script.test.ts @@ -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'); + }); +});