Test Fix: 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. 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.
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-01-15 08:55:38 -06:00
parent 38fa0fe99d
commit 54644df9a3
6 changed files with 281 additions and 421 deletions

View file

@ -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

View file

@ -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

View file

@ -293,6 +293,7 @@ describe('dateTimeUtils', () => {
const converted = result.current.toUserTime(null);
expect(converted).toBeDefined();
expect(converted.isValid()).toBe(false);
});

View file

@ -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();
});

View file

@ -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';

View file

@ -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');
});
});
});