diff --git a/CHANGELOG.md b/CHANGELOG.md index eca414a612..2772fb4f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ## Unreleased +* feat: collect host id for non-cloud environments [#3575](https://github.com/open-telemetry/opentelemetry-js/pull/3575) @mwear + ### :boom: Breaking Change ### :rocket: (Enhancement) diff --git a/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts b/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts index 85bd717e54..9bd5e6cb5f 100644 --- a/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts +++ b/packages/opentelemetry-resources/src/platform/node/HostDetectorSync.ts @@ -20,6 +20,7 @@ import { DetectorSync, ResourceAttributes } from '../../types'; import { ResourceDetectionConfig } from '../../config'; import { arch, hostname } from 'os'; import { normalizeArch } from './utils'; +import { getMachineId } from './machine-id/getMachineId'; /** * HostDetectorSync detects the resources related to the host current process is @@ -31,7 +32,18 @@ class HostDetectorSync implements DetectorSync { [SemanticResourceAttributes.HOST_NAME]: hostname(), [SemanticResourceAttributes.HOST_ARCH]: normalizeArch(arch()), }; - return new Resource(attributes); + + return new Resource(attributes, this._getAsyncAttributes()); + } + + private _getAsyncAttributes(): Promise { + return getMachineId().then(machineId => { + const attributes: ResourceAttributes = {}; + if (machineId) { + attributes[SemanticResourceAttributes.HOST_ID] = machineId; + } + return attributes; + }); } } diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/execAsync.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/execAsync.ts new file mode 100644 index 0000000000..c362a41862 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/execAsync.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as child_process from 'child_process'; +import * as util from 'util'; + +export const execAsync = util.promisify(child_process.exec); diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-bsd.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-bsd.ts new file mode 100644 index 0000000000..edd04c7f1b --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-bsd.ts @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs/promises'; +import { execAsync } from './execAsync'; +import { diag } from '@opentelemetry/api'; + +export async function getMachineId(): Promise { + try { + const result = await fs.readFile('/etc/hostid', { encoding: 'utf8' }); + return result.trim(); + } catch (e) { + diag.debug(`error reading machine id: ${e}`); + } + + try { + const result = await execAsync('kenv -q smbios.system.uuid'); + return result.stdout.trim(); + } catch (e) { + diag.debug(`error reading machine id: ${e}`); + } + + return ''; +} diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-darwin.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-darwin.ts new file mode 100644 index 0000000000..7f6975ef54 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-darwin.ts @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execAsync } from './execAsync'; +import { diag } from '@opentelemetry/api'; + +export async function getMachineId(): Promise { + try { + const result = await execAsync('ioreg -rd1 -c "IOPlatformExpertDevice"'); + + const idLine = result.stdout + .split('\n') + .find(line => line.includes('IOPlatformUUID')); + + if (!idLine) { + return ''; + } + + const parts = idLine.split('" = "'); + if (parts.length === 2) { + return parts[1].slice(0, -1); + } + } catch (e) { + diag.debug(`error reading machine id: ${e}`); + } + + return ''; +} diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-linux.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-linux.ts new file mode 100644 index 0000000000..74b145a2f4 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-linux.ts @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as fs from 'fs/promises'; +import { diag } from '@opentelemetry/api'; + +export async function getMachineId(): Promise { + const paths = ['/etc/machine-id', '/var/lib/dbus/machine-id']; + + for (const path of paths) { + try { + const result = await fs.readFile(path, { encoding: 'utf8' }); + return result.trim(); + } catch (e) { + diag.debug(`error reading machine id: ${e}`); + } + } + + return ''; +} diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-unsupported.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-unsupported.ts new file mode 100644 index 0000000000..cc05ca1c2e --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-unsupported.ts @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { diag } from '@opentelemetry/api'; + +export async function getMachineId(): Promise { + diag.debug('could not read machine-id: unsupported platform'); + return ''; +} diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-win.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-win.ts new file mode 100644 index 0000000000..05e9d3b7c1 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId-win.ts @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as process from 'process'; +import { execAsync } from './execAsync'; +import { diag } from '@opentelemetry/api'; + +export async function getMachineId(): Promise { + const args = + 'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid'; + let command = '%windir%\\System32\\REG.exe'; + if (process.arch === 'ia32' && 'PROCESSOR_ARCHITEW6432' in process.env) { + command = '%windir%\\sysnative\\cmd.exe /c ' + command; + } + + try { + const result = await execAsync(`${command} ${args}`); + const parts = result.stdout.split('REG_SZ'); + if (parts.length === 2) { + return parts[1].trim(); + } + } catch (e) { + diag.debug(`error reading machine id: ${e}`); + } + + return ''; +} diff --git a/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId.ts b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId.ts new file mode 100644 index 0000000000..47de5913b4 --- /dev/null +++ b/packages/opentelemetry-resources/src/platform/node/machine-id/getMachineId.ts @@ -0,0 +1,37 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as process from 'process'; + +let getMachineId: () => Promise; + +switch (process.platform) { + case 'darwin': + ({ getMachineId } = require('./getMachineId-darwin')); + break; + case 'linux': + ({ getMachineId } = require('./getMachineId-linux')); + break; + case 'freebsd': + ({ getMachineId } = require('./getMachineId-bsd')); + break; + case 'win32': + ({ getMachineId } = require('./getMachineId-win')); + break; + default: + ({ getMachineId } = require('./getMachineId-unsupported')); +} + +export { getMachineId }; diff --git a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts index 287b98b10b..2e69de7e19 100644 --- a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts @@ -27,11 +27,16 @@ describeNode('hostDetector() on Node.js', () => { it('should return resource information about the host', async () => { const os = require('os'); + const mid = require('../../../src/platform/node/machine-id/getMachineId'); + + const expectedHostId = 'f2c668b579780554f70f72a063dc0864'; sinon.stub(os, 'arch').returns('x64'); sinon.stub(os, 'hostname').returns('opentelemetry-test'); + sinon.stub(mid, 'getMachineId').returns(Promise.resolve(expectedHostId)); const resource: IResource = await hostDetector.detect(); + await resource.waitForAsyncAttributes?.(); assert.strictEqual( resource.attributes[SemanticResourceAttributes.HOST_NAME], @@ -41,6 +46,10 @@ describeNode('hostDetector() on Node.js', () => { resource.attributes[SemanticResourceAttributes.HOST_ARCH], 'amd64' ); + assert.strictEqual( + resource.attributes[SemanticResourceAttributes.HOST_ID], + expectedHostId + ); }); it('should pass through arch string if unknown', async () => { @@ -55,4 +64,29 @@ describeNode('hostDetector() on Node.js', () => { 'some-unknown-arch' ); }); + + it('should handle missing machine id', async () => { + const os = require('os'); + const mid = require('../../../src/platform/node/machine-id/getMachineId'); + + sinon.stub(os, 'arch').returns('x64'); + sinon.stub(os, 'hostname').returns('opentelemetry-test'); + sinon.stub(mid, 'getMachineId').returns(Promise.resolve('')); + + const resource: IResource = await hostDetector.detect(); + await resource.waitForAsyncAttributes?.(); + + assert.strictEqual( + resource.attributes[SemanticResourceAttributes.HOST_NAME], + 'opentelemetry-test' + ); + assert.strictEqual( + resource.attributes[SemanticResourceAttributes.HOST_ARCH], + 'amd64' + ); + assert.strictEqual( + false, + SemanticResourceAttributes.HOST_ID in resource.attributes + ); + }); }); diff --git a/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-bsd.test.ts b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-bsd.test.ts new file mode 100644 index 0000000000..317132a968 --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-bsd.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import * as fs from 'fs/promises'; +import { PromiseWithChild } from 'child_process'; +import * as util from '../../../../src/platform/node/machine-id/execAsync'; +import { getMachineId } from '../../../../src/platform/node/machine-id/getMachineId-bsd'; + +describe('getMachineId on BSD', () => { + const expectedMachineId = 'f2c668b579780554f70f72a063dc0864'; + const fileContents = `${expectedMachineId}\n`; + + afterEach(() => { + sinon.restore(); + }); + + it('reads machine-id from primary', async () => { + const stub = sinon + .stub(fs, 'readFile') + .returns(Promise.resolve(fileContents)); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(stub.callCount, 1); + }); + + it('reads machine-id from fallback when primary fails', async () => { + const fsStub = sinon + .stub(fs, 'readFile') + .returns(Promise.reject(new Error('not found'))); + + const execStub = sinon.stub(util, 'execAsync').returns( + Promise.resolve({ + stdout: fileContents, + stderr: '', + }) as PromiseWithChild<{ stdout: string; stderr: string }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(fsStub.callCount, 1); + assert.equal(execStub.callCount, 1); + }); + + it('handles failure to read primary and fallback', async () => { + const fsStub = sinon + .stub(fs, 'readFile') + .returns(Promise.reject(new Error('not found'))); + + const execStub = sinon.stub(util, 'execAsync').returns( + Promise.reject(new Error('not found')) as PromiseWithChild<{ + stdout: string; + stderr: string; + }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, ''); + assert.equal(fsStub.callCount, 1); + assert.equal(execStub.callCount, 1); + }); +}); diff --git a/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-darwin.test.ts b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-darwin.test.ts new file mode 100644 index 0000000000..06e1f942f8 --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-darwin.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { PromiseWithChild } from 'child_process'; +import * as util from '../../../../src/platform/node/machine-id/execAsync'; +import { getMachineId } from '../../../../src/platform/node/machine-id/getMachineId-darwin'; + +describe('getMachineId on Darwin', () => { + const expectedMachineId = '81895B8D-9EF9-4EBB-B5DE-B00069CF53F0'; + const cmdOutput = + '+-o J316sAP \n' + + '{\n' + + ' "IOPolledInterface" = "AppleARMWatchdogTimerHibernateHandler is not serializable"\n' + + ' "#address-cells" = <02000000>\n' + + ' "AAPL,phandle" = <01000000>\n' + + ' "serial-number" = <94e1c79ec04cd3f153f600000000000000000000000000000000000000000000>\n' + + ' "IOBusyInterest" = "IOCommand is not serializable"\n' + + ' "target-type" = <"J316s">\n' + + ' "platform-name" = <7436303030000000000000000000000000000000000000000000000000000000>\n' + + ' "secure-root-prefix" = <"md">\n' + + ' "name" = <"device-tree">\n' + + ' "region-info" = <4c4c2f4100000000000000000000000000000000000000000000000000000000>\n' + + ' "manufacturer" = <"Apple Inc.">\n' + + ' "compatible" = <"J316sAP","MacBookPro18,1","AppleARM">\n' + + ' "config-number" = <00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000>\n' + + ' "IOPlatformSerialNumber" = "HDWLIF2LM7"\n' + + ' "regulatory-model-number" = <4132343835000000000000000000000000000000000000000000000000000000>\n' + + ' "time-stamp" = <"Fri Aug 5 20:25:38 PDT 2022">\n' + + ' "clock-frequency" = <00366e01>\n' + + ' "model" = <"MacBookPro18,1">\n' + + ' "mlb-serial-number" = <5c92d268d6cd789e475ffafc0d363fc950000000000000000000000000000000>\n' + + ' "model-number" = <5a31345930303136430000000000000000000000000000000000000000000000>\n' + + ' "IONWInterrupts" = "IONWInterrupts"\n' + + ' "model-config" = <"ICT;MoPED=0x03D053A605C84ED11C455A18D6C643140B41A239">\n' + + ' "device_type" = <"bootrom">\n' + + ' "#size-cells" = <02000000>\n' + + ' "IOPlatformUUID" = "81895B8D-9EF9-4EBB-B5DE-B00069CF53F0"\n' + + '}\n'; + + afterEach(() => { + sinon.restore(); + }); + + it('reads machine-id', async () => { + const stub = sinon.stub(util, 'execAsync').returns( + Promise.resolve({ stdout: cmdOutput, stderr: '' }) as PromiseWithChild<{ + stdout: string; + stderr: string; + }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(stub.callCount, 1); + }); + + it('handles failure', async () => { + const stub = sinon.stub(util, 'execAsync').returns( + Promise.reject(new Error('not found')) as PromiseWithChild<{ + stdout: string; + stderr: string; + }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, ''); + assert.equal(stub.callCount, 1); + }); +}); diff --git a/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-linux.test.ts b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-linux.test.ts new file mode 100644 index 0000000000..d8dfe4d1da --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-linux.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import * as fs from 'fs/promises'; + +import { getMachineId } from '../../../../src/platform/node/machine-id/getMachineId-linux'; + +describe('getMachineId on linux', () => { + const expectedMachineId = 'f2c668b579780554f70f72a063dc0864'; + const fileContents = `${expectedMachineId}\n`; + + afterEach(() => { + sinon.restore(); + }); + + it('reads machine-id from primary', async () => { + const stub = sinon + .stub(fs, 'readFile') + .returns(Promise.resolve(fileContents)); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(stub.callCount, 1); + }); + + it('reads machine-id from fallback when primary fails', async () => { + const stub = sinon + .stub(fs, 'readFile') + .onFirstCall() + .returns(Promise.reject(new Error('not found'))) + .onSecondCall() + .returns(Promise.resolve(fileContents)); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(stub.callCount, 2); + }); + + it('handles failure to read primary and fallback', async () => { + const stub = sinon + .stub(fs, 'readFile') + .returns(Promise.reject(new Error('not found'))); + + const machineId = await getMachineId(); + + assert.equal(machineId, ''); + assert.equal(stub.callCount, 2); + }); +}); diff --git a/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-win.test.ts b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-win.test.ts new file mode 100644 index 0000000000..fba7747fee --- /dev/null +++ b/packages/opentelemetry-resources/test/detectors/node/machine-id/getMachineId-win.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { PromiseWithChild } from 'child_process'; +import * as util from '../../../../src/platform/node/machine-id/execAsync'; +import { getMachineId } from '../../../../src/platform/node/machine-id/getMachineId-win'; + +describe('getMachineId on Windows', () => { + const expectedMachineId = 'fe11b90a-d48c-4c3b-b386-e40cc383fd30'; + const cmdOutput = + '\n MachineGuid REG_SZ fe11b90a-d48c-4c3b-b386-e40cc383fd30\n'; + + afterEach(() => { + sinon.restore(); + }); + + it('reads machine-id', async () => { + const stub = sinon.stub(util, 'execAsync').returns( + Promise.resolve({ stdout: cmdOutput, stderr: '' }) as PromiseWithChild<{ + stdout: string; + stderr: string; + }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, expectedMachineId); + assert.equal(stub.callCount, 1); + }); + + it('handles failure', async () => { + const stub = sinon.stub(util, 'execAsync').returns( + Promise.reject(new Error('not found')) as PromiseWithChild<{ + stdout: string; + stderr: string; + }> + ); + + const machineId = await getMachineId(); + + assert.equal(machineId, ''); + assert.equal(stub.callCount, 1); + }); +});