feat(browser-utils): add automatic OS version detection

Implements automatic detection of OS versions at runtime instead of
hardcoded values. This ensures the library reports accurate platform
versions to WhatsApp without manual updates.

Version Detection:
- Linux: Reads /etc/os-release for distribution version (e.g., '24.04.1')
- macOS: Converts Darwin kernel version to macOS version (e.g., Darwin 24.x → macOS 15.x)
- Windows: Uses os.release() directly (already returns correct format)

Security Fixes (from PR #62 review):
- Fix isValidBrowserPreset() to use Object.prototype.hasOwnProperty.call()
  to prevent matching inherited properties like 'toString' or 'constructor'
- Fix getPlatformId() signature to accept 'unknown' type for runtime safety

Other Improvements:
- Add FALLBACK_VERSIONS constant with updated 2025 versions
- Add DARWIN_TO_MACOS mapping for accurate macOS version conversion
- Export detectedOSVersions for debugging and logging
- Improve JSDoc documentation with accurate examples
- Add 36 comprehensive unit tests (up from 25)

The version is detected once at module load and cached for consistent
behavior throughout the application lifecycle.

https://claude.ai/code/session_018XbFWEYwCfeeXioFDLvSXV
This commit is contained in:
Claude
2026-01-30 15:26:46 +00:00
parent 10aad67861
commit d2e599a617
2 changed files with 348 additions and 37 deletions
+105 -12
View File
@@ -1,30 +1,44 @@
import { Browsers, getPlatformId, isValidBrowserPreset } from '../../Utils/browser-utils'
import { Browsers, getPlatformId, isValidBrowserPreset, detectedOSVersions } from '../../Utils/browser-utils'
describe('browser-utils', () => {
describe('Browsers', () => {
describe('ubuntu', () => {
it('should return correct tuple for Ubuntu', () => {
const result = Browsers.ubuntu('Chrome')
expect(result).toEqual(['Ubuntu', 'Chrome', '22.04.4'])
expect(result[0]).toBe('Ubuntu')
expect(result[1]).toBe('Chrome')
expect(result[2]).toMatch(/^\d+\.\d+/) // Version format like '24.04' or '24.04.1'
})
it('should handle empty browser string', () => {
const result = Browsers.ubuntu('')
expect(result).toEqual(['Ubuntu', '', '22.04.4'])
expect(result[0]).toBe('Ubuntu')
expect(result[1]).toBe('')
})
it('should use auto-detected or fallback version', () => {
const result = Browsers.ubuntu('Test')
// Version should be a non-empty string
expect(typeof result[2]).toBe('string')
expect(result[2].length).toBeGreaterThan(0)
})
})
describe('macOS', () => {
it('should return correct tuple for macOS', () => {
const result = Browsers.macOS('Safari')
expect(result).toEqual(['Mac OS', 'Safari', '14.4.1'])
expect(result[0]).toBe('Mac OS')
expect(result[1]).toBe('Safari')
expect(result[2]).toMatch(/^\d+\.\d+/) // Version format like '15.2'
})
})
describe('windows', () => {
it('should return correct tuple for Windows', () => {
const result = Browsers.windows('Edge')
expect(result).toEqual(['Windows', 'Edge', '10.0.22631'])
expect(result[0]).toBe('Windows')
expect(result[1]).toBe('Edge')
expect(result[2]).toMatch(/^\d+\.\d+\.\d+/) // Version format like '10.0.26100'
})
})
@@ -51,6 +65,41 @@ describe('browser-utils', () => {
// Should be a valid platform name
expect(['Ubuntu', 'Mac OS', 'Windows', 'AIX', 'Android', 'FreeBSD', 'OpenBSD', 'Solaris']).toContain(result[0])
})
it('should return a valid version string', () => {
const result = Browsers.appropriate('Test')
expect(result[2]).toBeDefined()
expect(typeof result[2]).toBe('string')
expect(result[2].length).toBeGreaterThan(0)
})
})
})
describe('detectedOSVersions', () => {
it('should expose detected versions', () => {
expect(detectedOSVersions).toBeDefined()
expect(typeof detectedOSVersions.ubuntu).toBe('string')
expect(typeof detectedOSVersions.macOS).toBe('string')
expect(typeof detectedOSVersions.windows).toBe('string')
expect(typeof detectedOSVersions.baileys).toBe('string')
})
it('should have valid version formats', () => {
// Ubuntu: X.X or X.X.X
expect(detectedOSVersions.ubuntu).toMatch(/^\d+\.\d+(\.\d+)?$/)
// macOS: X.X or X.X.X
expect(detectedOSVersions.macOS).toMatch(/^\d+\.\d+(\.\d+)?$/)
// Windows: X.X.XXXXX
expect(detectedOSVersions.windows).toMatch(/^\d+\.\d+\.\d+$/)
// Baileys: semver
expect(detectedOSVersions.baileys).toMatch(/^\d+\.\d+\.\d+$/)
})
it('should match Browsers preset versions', () => {
expect(Browsers.ubuntu('Test')[2]).toBe(detectedOSVersions.ubuntu)
expect(Browsers.macOS('Test')[2]).toBe(detectedOSVersions.macOS)
expect(Browsers.windows('Test')[2]).toBe(detectedOSVersions.windows)
expect(Browsers.baileys('Test')[2]).toBe(detectedOSVersions.baileys)
})
})
@@ -89,34 +138,37 @@ describe('browser-utils', () => {
})
it('should return default ID for undefined input', () => {
// @ts-expect-error - Testing runtime behavior with invalid input
const result = getPlatformId(undefined)
expect(result).toBe('1')
})
it('should return default ID for null input', () => {
// @ts-expect-error - Testing runtime behavior with invalid input
const result = getPlatformId(null)
expect(result).toBe('1')
})
it('should return default ID for number input', () => {
// @ts-expect-error - Testing runtime behavior with invalid input
const result = getPlatformId(123)
expect(result).toBe('1')
})
it('should return default ID for object input', () => {
// @ts-expect-error - Testing runtime behavior with invalid input
const result = getPlatformId({ browser: 'chrome' })
expect(result).toBe('1')
})
it('should return default ID for array input', () => {
const result = getPlatformId(['chrome'])
expect(result).toBe('1')
})
it('should always return a string', () => {
const validResult = getPlatformId('chrome')
const invalidResult = getPlatformId('nonexistent')
const undefinedResult = getPlatformId(undefined)
expect(typeof validResult).toBe('string')
expect(typeof invalidResult).toBe('string')
expect(typeof undefinedResult).toBe('string')
})
})
@@ -133,6 +185,7 @@ describe('browser-utils', () => {
expect(isValidBrowserPreset('invalid')).toBe(false)
expect(isValidBrowserPreset('UBUNTU')).toBe(false)
expect(isValidBrowserPreset('Ubuntu')).toBe(false)
expect(isValidBrowserPreset('MacOS')).toBe(false)
})
it('should return false for non-string values', () => {
@@ -141,6 +194,17 @@ describe('browser-utils', () => {
expect(isValidBrowserPreset(123)).toBe(false)
expect(isValidBrowserPreset({})).toBe(false)
expect(isValidBrowserPreset([])).toBe(false)
expect(isValidBrowserPreset(true)).toBe(false)
})
// Critical security test: inherited properties should not match
it('should return false for inherited Object properties (security)', () => {
expect(isValidBrowserPreset('toString')).toBe(false)
expect(isValidBrowserPreset('constructor')).toBe(false)
expect(isValidBrowserPreset('valueOf')).toBe(false)
expect(isValidBrowserPreset('hasOwnProperty')).toBe(false)
expect(isValidBrowserPreset('__proto__')).toBe(false)
expect(isValidBrowserPreset('prototype')).toBe(false)
})
it('should work as type guard', () => {
@@ -165,12 +229,11 @@ describe('browser-utils', () => {
it('should not throw on getPlatformId with any input', () => {
expect(() => getPlatformId('valid')).not.toThrow()
expect(() => getPlatformId('')).not.toThrow()
// @ts-expect-error - Testing runtime behavior
expect(() => getPlatformId(undefined)).not.toThrow()
// @ts-expect-error - Testing runtime behavior
expect(() => getPlatformId(null)).not.toThrow()
// @ts-expect-error - Testing runtime behavior
expect(() => getPlatformId({})).not.toThrow()
expect(() => getPlatformId([])).not.toThrow()
expect(() => getPlatformId(Symbol('test'))).not.toThrow()
})
it('all Browsers methods should return readonly-compatible tuples', () => {
@@ -182,5 +245,35 @@ describe('browser-utils', () => {
expect(result.every(item => typeof item === 'string')).toBe(true)
}
})
it('version strings should never be empty', () => {
const presets = ['ubuntu', 'macOS', 'windows', 'baileys', 'appropriate'] as const
for (const preset of presets) {
const result = Browsers[preset]('Test')
expect(result[2].length).toBeGreaterThan(0)
}
})
})
describe('version detection', () => {
it('should detect a reasonable Ubuntu version', () => {
const version = detectedOSVersions.ubuntu
// Should be at least 18.04 (oldest supported LTS)
const majorVersion = parseFloat(version)
expect(majorVersion).toBeGreaterThanOrEqual(18)
})
it('should detect a reasonable macOS version', () => {
const version = detectedOSVersions.macOS
// Should be at least macOS 10.x or 11+
const majorVersion = parseFloat(version)
expect(majorVersion).toBeGreaterThanOrEqual(10)
})
it('should detect a reasonable Windows version', () => {
const version = detectedOSVersions.windows
// Should start with 10. (Windows 10/11)
expect(version).toMatch(/^10\./)
})
})
})