diff --git a/frontend/src/utils/forms/__tests__/OutputProfileUtils.test.js b/frontend/src/utils/forms/__tests__/OutputProfileUtils.test.js new file mode 100644 index 00000000..86eb2c00 --- /dev/null +++ b/frontend/src/utils/forms/__tests__/OutputProfileUtils.test.js @@ -0,0 +1,210 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// ── Mocks ────────────────────────────────────────────────────────────────────── + +vi.mock('../../../api', () => ({ + default: { + addOutputProfile: vi.fn(), + updateOutputProfile: vi.fn(), + }, +})); + +const mockResolver = vi.fn(); +vi.mock('@hookform/resolvers/yup', () => ({ + yupResolver: vi.fn(() => mockResolver), +})); + +// ── Imports after mocks ──────────────────────────────────────────────────────── + +import API from '../../../api'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { + BUILT_IN_COMMANDS, + COMMAND_EXAMPLES, + toCommandSelection, + schema, + addOutputProfile, + updateOutputProfile, + getResolver, +} from '../OutputProfileUtils'; + +// ────────────────────────────────────────────────────────────────────────────── + +describe('OutputProfileUtils', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(yupResolver).mockReturnValue(mockResolver); + }); + + // ── Constants ────────────────────────────────────────────────────────────── + + describe('BUILT_IN_COMMANDS', () => { + it('includes the ffmpeg entry', () => { + expect(BUILT_IN_COMMANDS).toContainEqual({ + value: 'ffmpeg', + label: 'FFmpeg', + }); + }); + + it('includes the custom entry', () => { + expect(BUILT_IN_COMMANDS).toContainEqual({ + value: '__custom__', + label: 'Custom…', + }); + }); + }); + + describe('COMMAND_EXAMPLES', () => { + it('has a non-empty string example for ffmpeg', () => { + expect(typeof COMMAND_EXAMPLES.ffmpeg).toBe('string'); + expect(COMMAND_EXAMPLES.ffmpeg.length).toBeGreaterThan(0); + }); + }); + + // ── toCommandSelection ───────────────────────────────────────────────────── + + describe('toCommandSelection', () => { + it('returns the command value when it matches a non-custom built-in', () => { + expect(toCommandSelection('ffmpeg')).toBe('ffmpeg'); + }); + + it('returns "__custom__" when command is "__custom__"', () => { + // __custom__ is in BUILT_IN_COMMANDS but excluded by the o.value !== '__custom__' guard + expect(toCommandSelection('__custom__')).toBe('__custom__'); + }); + + it('returns "__custom__" for an unrecognized command string', () => { + expect(toCommandSelection('my-arbitrary-tool')).toBe('__custom__'); + }); + + it('returns "__custom__" for an empty string', () => { + expect(toCommandSelection('')).toBe('__custom__'); + }); + + it('returns "__custom__" for undefined', () => { + expect(toCommandSelection(undefined)).toBe('__custom__'); + }); + }); + + // ── schema ───────────────────────────────────────────────────────────────── + + describe('schema', () => { + it('validates a fully populated object', async () => { + await expect( + schema.validate({ + name: 'HD Profile', + command: 'ffmpeg', + parameters: '-c:v copy', + }) + ).resolves.toMatchObject({ + name: 'HD Profile', + command: 'ffmpeg', + parameters: '-c:v copy', + }); + }); + + it('validates when parameters is omitted (optional)', async () => { + await expect( + schema.validate({ name: 'HD Profile', command: 'ffmpeg' }) + ).resolves.toMatchObject({ name: 'HD Profile', command: 'ffmpeg' }); + }); + + it('rejects when name is missing', async () => { + await expect(schema.validate({ command: 'ffmpeg' })).rejects.toThrow( + 'Name is required' + ); + }); + + it('rejects when name is an empty string', async () => { + await expect( + schema.validate({ name: '', command: 'ffmpeg' }) + ).rejects.toThrow('Name is required'); + }); + + it('rejects when command is missing', async () => { + await expect(schema.validate({ name: 'HD Profile' })).rejects.toThrow( + 'Command is required' + ); + }); + + it('rejects when command is an empty string', async () => { + await expect( + schema.validate({ name: 'HD Profile', command: '' }) + ).rejects.toThrow('Command is required'); + }); + }); + + // ── addOutputProfile ─────────────────────────────────────────────────────── + + describe('addOutputProfile', () => { + it('calls API.addOutputProfile with the provided values', async () => { + const values = { name: 'New Profile', command: 'ffmpeg', parameters: '' }; + vi.mocked(API.addOutputProfile).mockResolvedValue({ id: 1, ...values }); + + await addOutputProfile(values); + + expect(API.addOutputProfile).toHaveBeenCalledWith(values); + expect(API.addOutputProfile).toHaveBeenCalledTimes(1); + }); + + it('returns the API response', async () => { + const values = { name: 'New Profile', command: 'ffmpeg' }; + const response = { id: 42, ...values }; + vi.mocked(API.addOutputProfile).mockResolvedValue(response); + + const result = await addOutputProfile(values); + + expect(result).toEqual(response); + }); + + it('propagates errors thrown by API.addOutputProfile', async () => { + vi.mocked(API.addOutputProfile).mockRejectedValue( + new Error('Network error') + ); + + await expect(addOutputProfile({})).rejects.toThrow('Network error'); + }); + }); + + // ── updateOutputProfile ──────────────────────────────────────────────────── + + describe('updateOutputProfile', () => { + it('calls API.updateOutputProfile with the provided values', async () => { + const values = { id: 1, name: 'Updated Profile', command: 'ffmpeg' }; + vi.mocked(API.updateOutputProfile).mockResolvedValue(values); + + await updateOutputProfile(values); + + expect(API.updateOutputProfile).toHaveBeenCalledWith(values); + expect(API.updateOutputProfile).toHaveBeenCalledTimes(1); + }); + + it('returns the API response', async () => { + const values = { id: 7, name: 'Updated Profile', command: 'ffmpeg' }; + vi.mocked(API.updateOutputProfile).mockResolvedValue(values); + + const result = await updateOutputProfile(values); + + expect(result).toEqual(values); + }); + + it('propagates errors thrown by API.updateOutputProfile', async () => { + vi.mocked(API.updateOutputProfile).mockRejectedValue( + new Error('Update failed') + ); + + await expect(updateOutputProfile({})).rejects.toThrow('Update failed'); + }); + }); + + // ── getResolver ──────────────────────────────────────────────────────────── + + describe('getResolver', () => { + it('returns the result of yupResolver called with schema', () => { + const resolver = getResolver(); + + expect(yupResolver).toHaveBeenCalledWith(schema); + expect(resolver).toBe(mockResolver); + }); + }); +}); diff --git a/frontend/src/utils/forms/__tests__/ServerGroupUtils.test.js b/frontend/src/utils/forms/__tests__/ServerGroupUtils.test.js new file mode 100644 index 00000000..db94a8cd --- /dev/null +++ b/frontend/src/utils/forms/__tests__/ServerGroupUtils.test.js @@ -0,0 +1,114 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// ── Mocks ────────────────────────────────────────────────────────────────────── + +vi.mock('../../../api', () => ({ + default: { + addServerGroup: vi.fn(), + updateServerGroup: vi.fn(), + }, +})); + +const mockResolver = vi.fn(); +vi.mock('@hookform/resolvers/yup', () => ({ + yupResolver: vi.fn(() => mockResolver), +})); + +// ── Imports after mocks ──────────────────────────────────────────────────────── + +import API from '../../../api'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { + getResolver, + addServerGroup, + updateServerGroup, +} from '../ServerGroupUtils'; + +// ────────────────────────────────────────────────────────────────────────────── + +describe('ServerGroupUtils', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(yupResolver).mockReturnValue(mockResolver); + }); + + // ── getResolver ──────────────────────────────────────────────────────────── + + describe('getResolver', () => { + it('calls yupResolver with a schema and returns the result', () => { + const resolver = getResolver(); + + expect(yupResolver).toHaveBeenCalledTimes(1); + expect(yupResolver).toHaveBeenCalledWith(expect.any(Object)); + expect(resolver).toBe(mockResolver); + }); + + it('returns a new resolver on each call', () => { + const resolverA = getResolver(); + const resolverB = getResolver(); + + expect(yupResolver).toHaveBeenCalledTimes(2); + expect(resolverA).toBe(mockResolver); + expect(resolverB).toBe(mockResolver); + }); + }); + + // ── addServerGroup ───────────────────────────────────────────────────────── + + describe('addServerGroup', () => { + it('calls API.addServerGroup with the provided values', async () => { + const values = { name: 'US East' }; + vi.mocked(API.addServerGroup).mockResolvedValue({ id: 1, ...values }); + + await addServerGroup(values); + + expect(API.addServerGroup).toHaveBeenCalledWith(values); + expect(API.addServerGroup).toHaveBeenCalledTimes(1); + }); + + it('returns the API response', async () => { + const values = { name: 'US East' }; + const response = { id: 1, ...values }; + vi.mocked(API.addServerGroup).mockResolvedValue(response); + + const result = await addServerGroup(values); + + expect(result).toEqual(response); + }); + + it('propagates errors thrown by API.addServerGroup', async () => { + vi.mocked(API.addServerGroup).mockRejectedValue(new Error('Network error')); + + await expect(addServerGroup({ name: 'Test' })).rejects.toThrow('Network error'); + }); + }); + + // ── updateServerGroup ────────────────────────────────────────────────────── + + describe('updateServerGroup', () => { + it('calls API.updateServerGroup with the provided values', async () => { + const values = { id: 5, name: 'EU West' }; + vi.mocked(API.updateServerGroup).mockResolvedValue(values); + + await updateServerGroup(values); + + expect(API.updateServerGroup).toHaveBeenCalledWith(values); + expect(API.updateServerGroup).toHaveBeenCalledTimes(1); + }); + + it('returns the API response', async () => { + const values = { id: 5, name: 'EU West' }; + vi.mocked(API.updateServerGroup).mockResolvedValue(values); + + const result = await updateServerGroup(values); + + expect(result).toEqual(values); + }); + + it('propagates errors thrown by API.updateServerGroup', async () => { + vi.mocked(API.updateServerGroup).mockRejectedValue(new Error('Update failed')); + + await expect(updateServerGroup({ id: 1, name: 'Test' })).rejects.toThrow('Update failed'); + }); + }); +}); diff --git a/frontend/src/utils/pages/__tests__/PluginsUtils.test.js b/frontend/src/utils/pages/__tests__/PluginsUtils.test.js index 7d175451..bc27e005 100644 --- a/frontend/src/utils/pages/__tests__/PluginsUtils.test.js +++ b/frontend/src/utils/pages/__tests__/PluginsUtils.test.js @@ -11,6 +11,9 @@ vi.mock('../../../api.js', () => ({ reloadPlugins: vi.fn(), deletePlugin: vi.fn(), getPluginDetailManifest: vi.fn(), + getPluginRepoSettings: vi.fn(), + updatePluginRepoSettings: vi.fn(), + previewPluginRepo: vi.fn(), }, })); @@ -344,4 +347,86 @@ describe('PluginsUtils', () => { expect(API.getPluginDetailManifest).toHaveBeenCalledWith(null, null); }); }); + + describe('getPluginRepoSettings', () => { + it('should call API getPluginRepoSettings', () => { + PluginsUtils.getPluginRepoSettings(); + + expect(API.getPluginRepoSettings).toHaveBeenCalledTimes(1); + }); + + it('should return API response', () => { + const mockResponse = { repos: [] }; + + API.getPluginRepoSettings.mockReturnValue(mockResponse); + + const result = PluginsUtils.getPluginRepoSettings(); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('updatePluginRepoSettings', () => { + it('should call API updatePluginRepoSettings with values', () => { + const values = { repos: ['https://example.com/repo.json'] }; + + PluginsUtils.updatePluginRepoSettings(values); + + expect(API.updatePluginRepoSettings).toHaveBeenCalledWith(values); + expect(API.updatePluginRepoSettings).toHaveBeenCalledTimes(1); + }); + + it('should return API response', () => { + const values = { repos: ['https://example.com/repo.json'] }; + const mockResponse = { success: true }; + + API.updatePluginRepoSettings.mockReturnValue(mockResponse); + + const result = PluginsUtils.updatePluginRepoSettings(values); + + expect(result).toEqual(mockResponse); + }); + }); + + describe('previewPluginRepo', () => { + it('should call API previewPluginRepo with url and publicKey', () => { + const url = 'https://example.com/repo.json'; + const publicKey = 'public-key'; + + PluginsUtils.previewPluginRepo(url, publicKey); + + expect(API.previewPluginRepo).toHaveBeenCalledWith(url, publicKey); + expect(API.previewPluginRepo).toHaveBeenCalledTimes(1); + }); + + it('should return API response', () => { + const url = 'https://example.com/repo.json'; + const publicKey = 'public-key'; + const mockResponse = { name: 'Test Repo', plugins: [] }; + + API.previewPluginRepo.mockReturnValue(mockResponse); + + const result = PluginsUtils.previewPluginRepo(url, publicKey); + + expect(result).toEqual(mockResponse); + }); + + it('should handle empty string url and publicKey', () => { + const url = ''; + const publicKey = ''; + + PluginsUtils.previewPluginRepo(url, publicKey); + + expect(API.previewPluginRepo).toHaveBeenCalledWith('', ''); + }); + + it('should handle null url and publicKey', () => { + const url = null; + const publicKey = null; + + PluginsUtils.previewPluginRepo(url, publicKey); + + expect(API.previewPluginRepo).toHaveBeenCalledWith(null, null); + }); + }); });