diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 828bdc43..b279a82a 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -2,9 +2,9 @@ name: Frontend Tests on: push: - branches: [ main ] + branches: [main, dev] pull_request: - branches: [ main ] + branches: [main, dev] jobs: test: @@ -21,15 +21,15 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'npm' cache-dependency-path: './frontend/package-lock.json' - name: Install dependencies run: npm ci -# - name: Run linter -# run: npm run lint + # - name: Run linter + # run: npm run lint - name: Run tests run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aa7a3f2..610c2ee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Frontend tests GitHub workflow now uses Node.js 24 (matching Dockerfile) and runs on both `main` and `dev` branch pushes and pull requests for comprehensive CI coverage. + ### Fixed - Fixed NumPy baseline detection in Docker entrypoint. Now calls `numpy.show_config()` directly with case-insensitive grep instead of incorrectly wrapping the output. +- Fixed SettingsUtils frontend tests for new grouped settings architecture. Updated test suite to properly verify grouped JSON settings (stream_settings, dvr_settings, etc.) instead of individual CharField settings, including tests for type conversions, array-to-CSV transformations, and special handling of proxy_settings and network_access. ## [0.17.0] - 2026-01-13 diff --git a/frontend/src/utils/__tests__/dateTimeUtils.test.js b/frontend/src/utils/__tests__/dateTimeUtils.test.js index 852501e9..54644dcd 100644 --- a/frontend/src/utils/__tests__/dateTimeUtils.test.js +++ b/frontend/src/utils/__tests__/dateTimeUtils.test.js @@ -293,6 +293,7 @@ describe('dateTimeUtils', () => { const converted = result.current.toUserTime(null); + expect(converted).toBeDefined(); expect(converted.isValid()).toBe(false); }); diff --git a/frontend/src/utils/cards/__tests__/StreamConnectionCardUtils.test.js b/frontend/src/utils/cards/__tests__/StreamConnectionCardUtils.test.js index f48f1c1c..92c028c9 100644 --- a/frontend/src/utils/cards/__tests__/StreamConnectionCardUtils.test.js +++ b/frontend/src/utils/cards/__tests__/StreamConnectionCardUtils.test.js @@ -14,16 +14,15 @@ describe('StreamConnectionCardUtils', () => { describe('getBufferingSpeedThreshold', () => { it('should return parsed buffering_speed from proxy settings', () => { const proxySetting = { - value: JSON.stringify({ buffering_speed: 2.5 }) + value: { buffering_speed: 2.5 } }; expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)).toBe(2.5); }); it('should return 1.0 for invalid JSON', () => { - const proxySetting = { value: 'invalid json' }; + const proxySetting = { value: { buffering_speed: 'invalid' } }; const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); expect(StreamConnectionCardUtils.getBufferingSpeedThreshold(proxySetting)).toBe(1.0); - expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); diff --git a/frontend/src/utils/networkUtils.js b/frontend/src/utils/networkUtils.js index d8131229..8efd2254 100644 --- a/frontend/src/utils/networkUtils.js +++ b/frontend/src/utils/networkUtils.js @@ -1,7 +1,9 @@ -export const IPV4_CIDR_REGEX = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/\d+$/; +// IPv4 CIDR regex - validates IP address and prefix length (0-32) +export const IPV4_CIDR_REGEX = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(3[0-2]|[12]?[0-9])$/; +// IPv6 CIDR regex - validates IPv6 address and prefix length (0-128) export const IPV6_CIDR_REGEX = - /(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?![:.\w]))(([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}(?![:.\w]))(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}:|:(:[A-F0-9]{1,4}){7})(?![:.\w])\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])/; + /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; export function formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; diff --git a/frontend/src/utils/pages/__tests__/SettingsUtils.test.js b/frontend/src/utils/pages/__tests__/SettingsUtils.test.js index 9bf20b13..1611c7d3 100644 --- a/frontend/src/utils/pages/__tests__/SettingsUtils.test.js +++ b/frontend/src/utils/pages/__tests__/SettingsUtils.test.js @@ -19,540 +19,393 @@ describe('SettingsUtils', () => { describe('checkSetting', () => { it('should call API checkSetting with values', async () => { const values = { key: 'test-setting', value: 'test-value' }; - await SettingsUtils.checkSetting(values); - expect(API.checkSetting).toHaveBeenCalledWith(values); expect(API.checkSetting).toHaveBeenCalledTimes(1); }); - - it('should return API response', async () => { - const values = { key: 'test-setting', value: 'test-value' }; - const mockResponse = { valid: true }; - - API.checkSetting.mockResolvedValue(mockResponse); - - const result = await SettingsUtils.checkSetting(values); - - expect(result).toEqual(mockResponse); - }); - - it('should propagate API errors', async () => { - const values = { key: 'test-setting', value: 'test-value' }; - const error = new Error('API error'); - - API.checkSetting.mockRejectedValue(error); - - await expect(SettingsUtils.checkSetting(values)).rejects.toThrow('API error'); - }); }); describe('updateSetting', () => { it('should call API updateSetting with values', async () => { const values = { id: 1, key: 'test-setting', value: 'new-value' }; - await SettingsUtils.updateSetting(values); - expect(API.updateSetting).toHaveBeenCalledWith(values); expect(API.updateSetting).toHaveBeenCalledTimes(1); }); - - it('should return API response', async () => { - const values = { id: 1, key: 'test-setting', value: 'new-value' }; - const mockResponse = { id: 1, value: 'new-value' }; - - API.updateSetting.mockResolvedValue(mockResponse); - - const result = await SettingsUtils.updateSetting(values); - - expect(result).toEqual(mockResponse); - }); - - it('should propagate API errors', async () => { - const values = { id: 1, key: 'test-setting', value: 'new-value' }; - const error = new Error('Update failed'); - - API.updateSetting.mockRejectedValue(error); - - await expect(SettingsUtils.updateSetting(values)).rejects.toThrow('Update failed'); - }); }); describe('createSetting', () => { it('should call API createSetting with values', async () => { const values = { key: 'new-setting', name: 'New Setting', value: 'value' }; - await SettingsUtils.createSetting(values); - expect(API.createSetting).toHaveBeenCalledWith(values); expect(API.createSetting).toHaveBeenCalledTimes(1); }); - - it('should return API response', async () => { - const values = { key: 'new-setting', name: 'New Setting', value: 'value' }; - const mockResponse = { id: 1, ...values }; - - API.createSetting.mockResolvedValue(mockResponse); - - const result = await SettingsUtils.createSetting(values); - - expect(result).toEqual(mockResponse); - }); - - it('should propagate API errors', async () => { - const values = { key: 'new-setting', name: 'New Setting', value: 'value' }; - const error = new Error('Create failed'); - - API.createSetting.mockRejectedValue(error); - - await expect(SettingsUtils.createSetting(values)).rejects.toThrow('Create failed'); - }); }); describe('rehashStreams', () => { it('should call API rehashStreams', async () => { await SettingsUtils.rehashStreams(); - expect(API.rehashStreams).toHaveBeenCalledWith(); expect(API.rehashStreams).toHaveBeenCalledTimes(1); }); - - it('should return API response', async () => { - const mockResponse = { success: true }; - - API.rehashStreams.mockResolvedValue(mockResponse); - - const result = await SettingsUtils.rehashStreams(); - - expect(result).toEqual(mockResponse); - }); - - it('should propagate API errors', async () => { - const error = new Error('Rehash failed'); - - API.rehashStreams.mockRejectedValue(error); - - await expect(SettingsUtils.rehashStreams()).rejects.toThrow('Rehash failed'); - }); }); describe('saveChangedSettings', () => { - it('should update existing settings', async () => { + it('should group stream settings correctly and update', async () => { const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'old-value' } + stream_settings: { + id: 1, + key: 'stream_settings', + value: { + default_user_agent: 5, + m3u_hash_key: 'channel_name' + } + } }; const changedSettings = { - 'setting-1': 'new-value' + default_user_agent: 7, + preferred_region: 'UK' }; - API.updateSetting.mockResolvedValue({ id: 1, value: 'new-value' }); + API.updateSetting.mockResolvedValue({}); await SettingsUtils.saveChangedSettings(settings, changedSettings); expect(API.updateSetting).toHaveBeenCalledWith({ id: 1, - key: 'setting-1', - value: 'new-value' + key: 'stream_settings', + value: { + default_user_agent: 7, + m3u_hash_key: 'channel_name', + preferred_region: 'UK' + } }); }); - it('should create new settings when not in settings object', async () => { - const settings = {}; + it('should convert m3u_hash_key array to comma-separated string', async () => { + const settings = { + stream_settings: { + id: 1, + key: 'stream_settings', + value: {} + } + }; const changedSettings = { - 'new-setting': 'value' + m3u_hash_key: ['channel_name', 'channel_number'] }; - API.createSetting.mockResolvedValue({ id: 1, key: 'new-setting', value: 'value' }); + API.updateSetting.mockResolvedValue({}); await SettingsUtils.saveChangedSettings(settings, changedSettings); - expect(API.createSetting).toHaveBeenCalledWith({ - key: 'new-setting', - name: 'new setting', - value: 'value' + expect(API.updateSetting).toHaveBeenCalledWith({ + id: 1, + key: 'stream_settings', + value: { + m3u_hash_key: 'channel_name,channel_number' + } }); }); - it('should create new settings when existing has no id', async () => { + it('should convert ID fields to integers', async () => { const settings = { - 'setting-1': { key: 'setting-1', value: 'old-value' } + stream_settings: { + id: 1, + key: 'stream_settings', + value: {} + } }; const changedSettings = { - 'setting-1': 'new-value' + default_user_agent: '5', + default_stream_profile: '3' }; - API.createSetting.mockResolvedValue({ id: 1, key: 'setting-1', value: 'new-value' }); + API.updateSetting.mockResolvedValue({}); await SettingsUtils.saveChangedSettings(settings, changedSettings); - expect(API.createSetting).toHaveBeenCalledWith({ - key: 'setting-1', - name: 'setting 1', - value: 'new-value' + expect(API.updateSetting).toHaveBeenCalledWith({ + id: 1, + key: 'stream_settings', + value: { + default_user_agent: 5, + default_stream_profile: 3 + } }); }); - it('should replace hyphens with spaces in name', async () => { - const settings = {}; - const changedSettings = { - 'multi-word-setting': 'value' - }; - - API.createSetting.mockResolvedValue({ id: 1 }); - - await SettingsUtils.saveChangedSettings(settings, changedSettings); - - expect(API.createSetting).toHaveBeenCalledWith({ - key: 'multi-word-setting', - name: 'multi word setting', - value: 'value' - }); - }); - - it('should throw error when update fails', async () => { + it('should preserve boolean types', async () => { const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'old-value' } + dvr_settings: { + id: 2, + key: 'dvr_settings', + value: {} + }, + stream_settings: { + id: 1, + key: 'stream_settings', + value: {} + } }; const changedSettings = { - 'setting-1': 'new-value' + comskip_enabled: true, + auto_import_mapped_files: false }; - API.updateSetting.mockResolvedValue(undefined); - - await expect( - SettingsUtils.saveChangedSettings(settings, changedSettings) - ).rejects.toThrow('Failed to update setting'); - }); - - it('should throw error when create fails', async () => { - const settings = {}; - const changedSettings = { - 'new-setting': 'value' - }; - - API.createSetting.mockResolvedValue(undefined); - - await expect( - SettingsUtils.saveChangedSettings(settings, changedSettings) - ).rejects.toThrow('Failed to create setting'); - }); - - it('should process multiple changed settings', async () => { - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'old-value-1' }, - 'setting-2': { id: 2, key: 'setting-2', value: 'old-value-2' } - }; - const changedSettings = { - 'setting-1': 'new-value-1', - 'setting-2': 'new-value-2', - 'setting-3': 'new-value-3' - }; - - API.updateSetting.mockResolvedValue({ success: true }); - API.createSetting.mockResolvedValue({ success: true }); + API.updateSetting.mockResolvedValue({}); await SettingsUtils.saveChangedSettings(settings, changedSettings); expect(API.updateSetting).toHaveBeenCalledTimes(2); - expect(API.createSetting).toHaveBeenCalledTimes(1); }); - it('should handle empty changedSettings', async () => { + it('should handle proxy_settings specially', async () => { const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'value' } + proxy_settings: { + id: 5, + key: 'proxy_settings', + value: { + buffering_speed: 1.0 + } + } }; - const changedSettings = {}; + const changedSettings = { + proxy_settings: { + buffering_speed: 2.5, + buffering_timeout: 15 + } + }; + + API.updateSetting.mockResolvedValue({}); await SettingsUtils.saveChangedSettings(settings, changedSettings); - expect(API.updateSetting).not.toHaveBeenCalled(); - expect(API.createSetting).not.toHaveBeenCalled(); - }); - }); - - describe('getChangedSettings', () => { - it('should detect changed values', () => { - const values = { - 'setting-1': 'new-value' - }; - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'old-value' } - }; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({ - 'setting-1': 'new-value' + expect(API.updateSetting).toHaveBeenCalledWith({ + id: 5, + key: 'proxy_settings', + value: { + buffering_speed: 2.5, + buffering_timeout: 15 + } }); }); - it('should include new settings not in settings object', () => { - const values = { - 'new-setting': 'value' - }; + it('should create proxy_settings if it does not exist', async () => { const settings = {}; + const changedSettings = { + proxy_settings: { + buffering_speed: 2.5 + } + }; - const result = SettingsUtils.getChangedSettings(values, settings); + API.createSetting.mockResolvedValue({}); - expect(result).toEqual({ - 'new-setting': 'value' + await SettingsUtils.saveChangedSettings(settings, changedSettings); + + expect(API.createSetting).toHaveBeenCalledWith({ + key: 'proxy_settings', + name: 'Proxy Settings', + value: { + buffering_speed: 2.5 + } }); }); - it('should skip unchanged values', () => { - const values = { - 'setting-1': 'same-value' - }; + it('should handle network_access specially', async () => { const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'same-value' } + network_access: { + id: 6, + key: 'network_access', + value: [] + } + }; + const changedSettings = { + network_access: ['192.168.1.0/24', '10.0.0.0/8'] }; - const result = SettingsUtils.getChangedSettings(values, settings); + API.updateSetting.mockResolvedValue({}); - expect(result).toEqual({}); - }); + await SettingsUtils.saveChangedSettings(settings, changedSettings); - it('should convert array values to comma-separated strings', () => { - const values = { - 'm3u-hash-key': ['key1', 'key2', 'key3'] - }; - const settings = { - 'm3u-hash-key': { id: 1, key: 'm3u-hash-key', value: 'old-value' } - }; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({ - 'm3u-hash-key': 'key1,key2,key3' + expect(API.updateSetting).toHaveBeenCalledWith({ + id: 6, + key: 'network_access', + value: ['192.168.1.0/24', '10.0.0.0/8'] }); }); - - it('should skip empty string values', () => { - const values = { - 'setting-1': '', - 'setting-2': 'value' - }; - const settings = {}; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({ - 'setting-2': 'value' - }); - }); - - it('should skip empty array values', () => { - const values = { - 'setting-1': [], - 'setting-2': ['value'] - }; - const settings = {}; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({ - 'setting-2': 'value' - }); - }); - - it('should convert non-string values to strings', () => { - const values = { - 'setting-1': 123, - 'setting-2': true, - 'setting-3': false - }; - const settings = {}; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({ - 'setting-1': '123', - 'setting-2': 'true', - 'setting-3': 'false' - }); - }); - - it('should compare string values correctly', () => { - const values = { - 'setting-1': 'value', - 'setting-2': 123 - }; - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'value' }, - 'setting-2': { id: 2, key: 'setting-2', value: 123 } - }; - - const result = SettingsUtils.getChangedSettings(values, settings); - - expect(result).toEqual({}); - }); }); describe('parseSettings', () => { - it('should convert string "true" to boolean true', () => { - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'true' } + it('should parse grouped settings correctly', () => { + const mockSettings = { + 'stream_settings': { + id: 1, + key: 'stream_settings', + value: { + default_user_agent: 5, + default_stream_profile: 3, + m3u_hash_key: 'channel_name,channel_number', + preferred_region: 'US', + auto_import_mapped_files: true + } + }, + 'dvr_settings': { + id: 2, + key: 'dvr_settings', + value: { + tv_template: '/media/tv/{show}/{season}/', + comskip_enabled: false, + pre_offset_minutes: 2, + post_offset_minutes: 5 + } + } }; - const result = SettingsUtils.parseSettings(settings); + const result = SettingsUtils.parseSettings(mockSettings); - expect(result).toEqual({ - 'setting-1': true + // Check stream settings + expect(result.default_user_agent).toBe('5'); + expect(result.default_stream_profile).toBe('3'); + expect(result.m3u_hash_key).toEqual(['channel_name', 'channel_number']); + expect(result.preferred_region).toBe('US'); + expect(result.auto_import_mapped_files).toBe(true); + + // Check DVR settings + expect(result.tv_template).toBe('/media/tv/{show}/{season}/'); + expect(result.comskip_enabled).toBe(false); + expect(result.pre_offset_minutes).toBe(2); + expect(result.post_offset_minutes).toBe(5); + }); + + it('should handle empty m3u_hash_key', () => { + const mockSettings = { + 'stream_settings': { + id: 1, + key: 'stream_settings', + value: { + m3u_hash_key: '' + } + } + }; + + const result = SettingsUtils.parseSettings(mockSettings); + expect(result.m3u_hash_key).toEqual([]); + }); + + it('should handle proxy_settings', () => { + const mockSettings = { + 'proxy_settings': { + id: 5, + key: 'proxy_settings', + value: { + buffering_speed: 2.5, + buffering_timeout: 15 + } + } + }; + + const result = SettingsUtils.parseSettings(mockSettings); + expect(result.proxy_settings).toEqual({ + buffering_speed: 2.5, + buffering_timeout: 15 }); }); - it('should convert string "false" to boolean false', () => { - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'false' } + it('should handle network_access', () => { + const mockSettings = { + 'network_access': { + id: 6, + key: 'network_access', + value: ['192.168.1.0/24', '10.0.0.0/8'] + } }; - const result = SettingsUtils.parseSettings(settings); + const result = SettingsUtils.parseSettings(mockSettings); + expect(result.network_access).toEqual(['192.168.1.0/24', '10.0.0.0/8']); + }); + }); - expect(result).toEqual({ - 'setting-1': false + describe('getChangedSettings', () => { + it('should detect changes in primitive values', () => { + const values = { + time_zone: 'America/New_York', + max_system_events: 2000, + comskip_enabled: true + }; + const settings = { + time_zone: { value: 'UTC' }, + max_system_events: { value: 1000 }, + comskip_enabled: { value: false } + }; + + const changes = SettingsUtils.getChangedSettings(values, settings); + + expect(changes).toEqual({ + time_zone: 'America/New_York', + max_system_events: 2000, + comskip_enabled: true }); }); - it('should parse m3u-hash-key as array', () => { + it('should not detect unchanged values', () => { + const values = { + time_zone: 'UTC', + max_system_events: 1000 + }; const settings = { - 'm3u-hash-key': { id: 1, key: 'm3u-hash-key', value: 'key1,key2,key3' } + time_zone: { value: 'UTC' }, + max_system_events: { value: 1000 } }; - const result = SettingsUtils.parseSettings(settings); + const changes = SettingsUtils.getChangedSettings(values, settings); + expect(changes).toEqual({}); + }); - expect(result).toEqual({ - 'm3u-hash-key': ['key1', 'key2', 'key3'] + it('should preserve type of numeric values', () => { + const values = { + max_system_events: 2000 + }; + const settings = { + max_system_events: { value: 1000 } + }; + + const changes = SettingsUtils.getChangedSettings(values, settings); + expect(typeof changes.max_system_events).toBe('number'); + expect(changes.max_system_events).toBe(2000); + }); + + it('should detect changes in array values', () => { + const values = { + m3u_hash_key: ['channel_name', 'channel_number'] + }; + const settings = { + m3u_hash_key: { value: 'channel_name' } + }; + + const changes = SettingsUtils.getChangedSettings(values, settings); + // Arrays are converted to comma-separated strings internally + expect(changes).toEqual({ + m3u_hash_key: 'channel_name,channel_number' }); }); - it('should filter empty strings from m3u-hash-key array', () => { + it('should skip proxy_settings and network_access', () => { + const values = { + time_zone: 'America/New_York', + proxy_settings: { + buffering_speed: 2.5 + }, + network_access: ['192.168.1.0/24'] + }; const settings = { - 'm3u-hash-key': { id: 1, key: 'm3u-hash-key', value: 'key1,,key2,' } + time_zone: { value: 'UTC' } }; - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'm3u-hash-key': ['key1', 'key2'] - }); - }); - - it('should return empty array for empty m3u-hash-key', () => { - const settings = { - 'm3u-hash-key': { id: 1, key: 'm3u-hash-key', value: '' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'm3u-hash-key': [] - }); - }); - - it('should return empty array for null m3u-hash-key', () => { - const settings = { - 'm3u-hash-key': { id: 1, key: 'm3u-hash-key', value: null } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'm3u-hash-key': [] - }); - }); - - it('should parse dvr-pre-offset-minutes as integer', () => { - const settings = { - 'dvr-pre-offset-minutes': { id: 1, key: 'dvr-pre-offset-minutes', value: '5' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'dvr-pre-offset-minutes': 5 - }); - }); - - it('should parse dvr-post-offset-minutes as integer', () => { - const settings = { - 'dvr-post-offset-minutes': { id: 1, key: 'dvr-post-offset-minutes', value: '10' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'dvr-post-offset-minutes': 10 - }); - }); - - it('should default offset minutes to 0 for empty string', () => { - const settings = { - 'dvr-pre-offset-minutes': { id: 1, key: 'dvr-pre-offset-minutes', value: '' }, - 'dvr-post-offset-minutes': { id: 2, key: 'dvr-post-offset-minutes', value: '' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'dvr-pre-offset-minutes': 0, - 'dvr-post-offset-minutes': 0 - }); - }); - - it('should default offset minutes to 0 for NaN', () => { - const settings = { - 'dvr-pre-offset-minutes': { id: 1, key: 'dvr-pre-offset-minutes', value: 'invalid' }, - 'dvr-post-offset-minutes': { id: 2, key: 'dvr-post-offset-minutes', value: 'abc' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'dvr-pre-offset-minutes': 0, - 'dvr-post-offset-minutes': 0 - }); - }); - - it('should keep other values unchanged', () => { - const settings = { - 'setting-1': { id: 1, key: 'setting-1', value: 'test-value' }, - 'setting-2': { id: 2, key: 'setting-2', value: 123 } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'setting-1': 'test-value', - 'setting-2': 123 - }); - }); - - it('should handle empty settings object', () => { - const result = SettingsUtils.parseSettings({}); - - expect(result).toEqual({}); - }); - - it('should process multiple settings with mixed types', () => { - const settings = { - 'enabled': { id: 1, key: 'enabled', value: 'true' }, - 'disabled': { id: 2, key: 'disabled', value: 'false' }, - 'm3u-hash-key': { id: 3, key: 'm3u-hash-key', value: 'key1,key2' }, - 'dvr-pre-offset-minutes': { id: 4, key: 'dvr-pre-offset-minutes', value: '5' }, - 'dvr-post-offset-minutes': { id: 5, key: 'dvr-post-offset-minutes', value: '10' }, - 'other-setting': { id: 6, key: 'other-setting', value: 'value' } - }; - - const result = SettingsUtils.parseSettings(settings); - - expect(result).toEqual({ - 'enabled': true, - 'disabled': false, - 'm3u-hash-key': ['key1', 'key2'], - 'dvr-pre-offset-minutes': 5, - 'dvr-post-offset-minutes': 10, - 'other-setting': 'value' - }); + const changes = SettingsUtils.getChangedSettings(values, settings); + expect(changes.proxy_settings).toBeUndefined(); + expect(changes.network_access).toBeUndefined(); + expect(changes.time_zone).toBe('America/New_York'); }); }); });