mirror of
https://github.com/unraid/api.git
synced 2025-12-30 21:19:49 -06:00
test: create tests for stores batch 3 (#1358)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added comprehensive test coverage for the purchase, replaceRenew, modal, notifications, theme, trial, unraidApi, unraidApiSettings, updateOs, updateOsActions, updateOsChangelog, activationCode, and callbackActions stores. - Exposed callback error state in the callbackActions store for external access. - Made error state publicly accessible in the replaceRenew store. - **Tests** - Introduced new test files covering state, getters, actions, and side effects for multiple stores including modal, notifications, purchase, replaceRenew, theme, trial, unraidApi, unraidApiSettings, updateOs, updateOsActions, updateOsChangelog, activationCode, and callbackActions. - Enhanced existing test suites with additional mocks, reactive state handling, and expanded test cases for improved coverage and robustness. - **Refactor** - Improved code clarity and readability in modal, notifications, purchase, replaceRenew, trial, theme, updateOsActions, callbackActions, and unraidApi stores through import reorganization and formatting adjustments. - Updated imports to include reactive and computed utilities for enhanced state management in several stores. - Standardized import styles and streamlined store definitions in the unraidApiSettings store. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net>
This commit is contained in:
@@ -8,8 +8,13 @@ import { createPinia, setActivePinia } from 'pinia';
|
||||
import { ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY } from '~/consts';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { QueryPayloads } from '@unraid/shared-callbacks';
|
||||
|
||||
import { useActivationCodeStore } from '~/store/activationCode';
|
||||
|
||||
// Mock the shared-callbacks module to prevent crypto-js issues in test
|
||||
vi.mock('@unraid/shared-callbacks', () => ({}));
|
||||
|
||||
// Mock console methods to suppress output
|
||||
const originalConsoleDebug = console.debug;
|
||||
const originalConsoleError = console.error;
|
||||
@@ -24,7 +29,6 @@ afterAll(() => {
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
// Mock sessionStorage
|
||||
const mockStorage = new Map<string, string>();
|
||||
vi.stubGlobal('sessionStorage', {
|
||||
getItem: (key: string) => mockStorage.get(key) ?? null,
|
||||
@@ -34,27 +38,19 @@ vi.stubGlobal('sessionStorage', {
|
||||
});
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('pinia', async () => {
|
||||
const mod = await vi.importActual<typeof import('pinia')>('pinia');
|
||||
|
||||
return {
|
||||
...mod,
|
||||
storeToRefs: () => ({
|
||||
state: ref('ENOKEYFILE'),
|
||||
callbackData: ref(null),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// NOTE: Mocks need to be hoistable, so use factory functions
|
||||
// We need refs here to allow changing the values in tests
|
||||
const mockServerState = ref('ENOKEYFILE');
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => ({
|
||||
state: 'ENOKEYFILE',
|
||||
state: mockServerState,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockCallbackData = ref<QueryPayloads | null>(null);
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
callbackData: null,
|
||||
callbackData: mockCallbackData,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -64,6 +60,9 @@ describe('Activation Code Store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
|
||||
mockServerState.value = 'ENOKEYFILE';
|
||||
mockCallbackData.value = null;
|
||||
|
||||
store = useActivationCodeStore();
|
||||
vi.clearAllMocks();
|
||||
mockStorage.clear();
|
||||
@@ -106,11 +105,25 @@ describe('Activation Code Store', () => {
|
||||
describe('Modal Visibility', () => {
|
||||
it('should show activation modal by default when conditions are met', () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show modal when data is null', () => {
|
||||
// store.data is null by default after beforeEach resets
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show modal when server state is not ENOKEYFILE', async () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
mockServerState.value = 'RUNNING';
|
||||
await nextTick();
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show modal when callback data exists', async () => {
|
||||
store.setData({ code: 'TEST123' });
|
||||
mockCallbackData.value = { some: 'data' } as unknown as QueryPayloads;
|
||||
await nextTick();
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
});
|
||||
|
||||
@@ -119,16 +132,31 @@ describe('Activation Code Store', () => {
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
|
||||
store.setActivationModalHidden(true);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
expect(sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY)).toBe('true');
|
||||
|
||||
store.setActivationModalHidden(false);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.showActivationModal).toBe(true);
|
||||
expect(sessionStorage.getItem(ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY)).toBeNull();
|
||||
|
||||
// Set data again before hiding and changing other states
|
||||
store.setData({ code: 'TEST123' });
|
||||
store.setActivationModalHidden(true);
|
||||
|
||||
await nextTick();
|
||||
|
||||
mockServerState.value = 'STOPPED';
|
||||
mockCallbackData.value = { other: 'info' } as unknown as QueryPayloads;
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.showActivationModal).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { Mock } from 'vitest';
|
||||
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useInstallKeyStore } from '~/store/installKey';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
|
||||
@@ -122,6 +123,18 @@ vi.mock('~/store/updateOsActions', () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/updateOs', () => {
|
||||
return {
|
||||
useUpdateOsStore: vi.fn(() => ({
|
||||
$state: {},
|
||||
$patch: vi.fn(),
|
||||
$reset: vi.fn(),
|
||||
$subscribe: vi.fn(),
|
||||
$dispose: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Callback Actions Store', () => {
|
||||
let store: ReturnType<typeof useCallbackActionsStore>;
|
||||
let preventClose: { addPreventClose: Mock; removePreventClose: Mock };
|
||||
@@ -218,7 +231,7 @@ describe('Callback Actions Store', () => {
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
});
|
||||
|
||||
it('should handle invalid callback type', () => {
|
||||
it('should handle invalid callback type', async () => {
|
||||
const mockData = {
|
||||
type: 'fromUpc',
|
||||
actions: [],
|
||||
@@ -227,12 +240,14 @@ describe('Callback Actions Store', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error');
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[redirectToCallbackType]',
|
||||
'Callback redirect type not present or incorrect'
|
||||
);
|
||||
expect(store.callbackStatus).toBe('ready');
|
||||
expect(store.$state.callbackError).toBe('Callback redirect type not present or incorrect');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,6 +296,25 @@ describe('Callback Actions Store', () => {
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle oemSignOut action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'oemSignOut',
|
||||
},
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(vi.mocked(useAccountStore)().setAccountAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useAccountStore)().setQueueConnectSignOut).toHaveBeenCalledWith(true);
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle updateOs action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
@@ -306,6 +340,32 @@ describe('Callback Actions Store', () => {
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).not.toHaveBeenCalled(); // Single action, no refresh needed
|
||||
});
|
||||
|
||||
it('should handle downgradeOs action', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'downgradeOs',
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
sha256: 'test-sha256',
|
||||
version: '6.11.5',
|
||||
} as ExternalUpdateOsAction,
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
const mockUpdateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(mockUpdateOsActionsStore.setUpdateOsAction).toHaveBeenCalled();
|
||||
expect(mockUpdateOsActionsStore.actOnUpdateOsAction).toHaveBeenCalledWith(true);
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).not.toHaveBeenCalled(); // Single action, no refresh needed
|
||||
});
|
||||
|
||||
it('should handle multiple actions', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
@@ -335,6 +395,28 @@ describe('Callback Actions Store', () => {
|
||||
expect(vi.mocked(useUpdateOsActionsStore)().setUpdateOsAction).toHaveBeenCalled();
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle key install action (e.g., purchase)', async () => {
|
||||
const mockData: QueryPayloads = {
|
||||
type: 'forUpc',
|
||||
actions: [
|
||||
{
|
||||
type: 'purchase',
|
||||
keyUrl: 'mock-key-url',
|
||||
},
|
||||
],
|
||||
sender: 'test',
|
||||
};
|
||||
const mockInstallKeyStore = useInstallKeyStore();
|
||||
|
||||
store.saveCallbackData(mockData);
|
||||
await nextTick();
|
||||
|
||||
expect(mockInstallKeyStore.install).toHaveBeenCalledWith(mockData.actions[0]);
|
||||
expect(vi.mocked(useAccountStore)().setAccountAction).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(useUpdateOsActionsStore)().setUpdateOsAction).not.toHaveBeenCalled();
|
||||
expect(vi.mocked(useServerStore)().refreshServerState).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Status Management', () => {
|
||||
|
||||
@@ -13,12 +13,10 @@ import { useErrorsStore } from '~/store/errors';
|
||||
|
||||
const mockFeedbackButton = vi.fn();
|
||||
|
||||
// Mock OBJ_TO_STR function
|
||||
vi.mock('~/helpers/functions', () => ({
|
||||
OBJ_TO_STR: (obj: unknown) => JSON.stringify(obj),
|
||||
}));
|
||||
|
||||
// Mock FeedbackButton global
|
||||
vi.stubGlobal('FeedbackButton', mockFeedbackButton);
|
||||
|
||||
describe('Errors Store', () => {
|
||||
@@ -93,11 +91,34 @@ describe('Errors Store', () => {
|
||||
|
||||
expect(store.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not change errors when removing by non-existent index', async () => {
|
||||
store.setError(mockError);
|
||||
const initialErrors = [...store.errors];
|
||||
expect(initialErrors).toHaveLength(1);
|
||||
|
||||
store.removeErrorByIndex(99);
|
||||
await nextTick();
|
||||
|
||||
expect(store.errors).toHaveLength(1);
|
||||
expect(store.errors).toEqual(initialErrors);
|
||||
});
|
||||
|
||||
it('should not change errors when removing by non-existent ref', async () => {
|
||||
store.setError(mockError);
|
||||
const initialErrors = [...store.errors];
|
||||
expect(initialErrors).toHaveLength(1);
|
||||
|
||||
store.removeErrorByRef('non-existent-ref');
|
||||
await nextTick();
|
||||
|
||||
expect(store.errors).toHaveLength(1);
|
||||
expect(store.errors).toEqual(initialErrors);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Troubleshoot Feature', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the DOM elements needed for troubleshoot
|
||||
const mockModal = document.createElement('div');
|
||||
mockModal.className = 'sweet-alert visible';
|
||||
|
||||
@@ -126,8 +147,18 @@ describe('Errors Store', () => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should open troubleshoot with error details', async () => {
|
||||
store.setError(mockError);
|
||||
it('should open troubleshoot with multiple error details including debugServer', async () => {
|
||||
const error1: Error = { ...mockError, ref: 'err1' };
|
||||
const error2: Error = {
|
||||
heading: 'Second Error',
|
||||
level: 'warning',
|
||||
message: 'Another message',
|
||||
type: 'serverState',
|
||||
ref: 'err2',
|
||||
debugServer: { guid: 'debug-guid', name: 'debug-server' },
|
||||
};
|
||||
store.setError(error1);
|
||||
store.setError(error2);
|
||||
await nextTick();
|
||||
|
||||
await store.openTroubleshoot({
|
||||
@@ -141,12 +172,57 @@ describe('Errors Store', () => {
|
||||
const panel = document.querySelector('#troubleshoot_panel') as HTMLElement;
|
||||
|
||||
expect(mockFeedbackButton).toHaveBeenCalled();
|
||||
expect(textarea.value).toContain('Debug Details – Component Errors 1');
|
||||
expect(textarea.value).toContain('Debug Details – Component Errors 2');
|
||||
expect(textarea.value).toContain('Error 1: Test Error');
|
||||
expect(textarea.value).toContain('Error 1 Message: Test message');
|
||||
expect(textarea.value).toContain('Error 1 Ref: err1');
|
||||
expect(textarea.value).not.toContain('Error 1 Debug Server');
|
||||
expect(textarea.value).toContain('\n***************\n');
|
||||
expect(textarea.value).toContain('Error 2: Second Error');
|
||||
expect(textarea.value).toContain('Error 2 Message: Another message');
|
||||
expect(textarea.value).toContain('Error 2 Level: warning');
|
||||
expect(textarea.value).toContain('Error 2 Type: serverState');
|
||||
expect(textarea.value).toContain('Error 2 Ref: err2');
|
||||
expect(textarea.value).toContain(
|
||||
'Error 2 Debug Server:\n{"guid":"debug-guid","name":"debug-server"}'
|
||||
);
|
||||
|
||||
expect(emailInput.value).toBe('test@example.com');
|
||||
expect(radio.checked).toBe(true);
|
||||
expect(panel.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('should focus email input if no email provided', async () => {
|
||||
const focusSpy = vi.spyOn(HTMLInputElement.prototype, 'focus');
|
||||
|
||||
await store.openTroubleshoot({
|
||||
email: '',
|
||||
includeUnraidApiLogs: true,
|
||||
});
|
||||
|
||||
const emailInput = document.querySelector('#troubleshootEmail') as HTMLInputElement;
|
||||
|
||||
expect(focusSpy).toHaveBeenCalled();
|
||||
expect(emailInput.value).toBe('');
|
||||
|
||||
focusSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should handle errors during troubleshoot opening', async () => {
|
||||
const testError = new Error('FeedbackButton failed');
|
||||
mockFeedbackButton.mockRejectedValueOnce(testError);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'error');
|
||||
|
||||
await store.openTroubleshoot({
|
||||
email: 'test@example.com',
|
||||
includeUnraidApiLogs: true,
|
||||
});
|
||||
|
||||
expect(mockFeedbackButton).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[openTroubleshoot]', testError);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,8 @@ describe('InstallKey Store', () => {
|
||||
|
||||
expect(store.keyInstallStatus).toBe('failed');
|
||||
expect(console.error).toHaveBeenCalledWith('[install] no key to install');
|
||||
expect(store.keyType).toBeUndefined();
|
||||
expect(store.keyUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set status to installing when install is called', async () => {
|
||||
@@ -85,17 +87,23 @@ describe('InstallKey Store', () => {
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should call WebguiInstallKey.query with correct url', async () => {
|
||||
it('should handle successful install and update state', async () => {
|
||||
mockGetFn.mockResolvedValueOnce({ success: true });
|
||||
const action = createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
});
|
||||
|
||||
await store.install(
|
||||
createTestAction({
|
||||
type: 'purchase',
|
||||
keyUrl: 'https://example.com/license.key',
|
||||
})
|
||||
);
|
||||
await store.install(action);
|
||||
|
||||
const { WebguiInstallKey } = await import('~/composables/services/webgui');
|
||||
|
||||
expect(WebguiInstallKey.query).toHaveBeenCalledWith({ url: action.keyUrl });
|
||||
expect(mockGetFn).toHaveBeenCalled();
|
||||
expect(store.keyInstallStatus).toBe('success');
|
||||
expect(store.keyActionType).toBe('purchase');
|
||||
expect(store.keyUrl).toBe('https://example.com/license.key');
|
||||
expect(store.keyType).toBe('license');
|
||||
});
|
||||
|
||||
it('should extract key type from .key URL', async () => {
|
||||
|
||||
56
web/__test__/store/modal.test.ts
Normal file
56
web/__test__/store/modal.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Modal store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useModalStore } from '~/store/modal';
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useToggle: vi.fn((value) => () => {
|
||||
value.value = !value.value;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Modal Store', () => {
|
||||
let store: ReturnType<typeof useModalStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useModalStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Initialization', () => {
|
||||
it('should initialize with modal visible', () => {
|
||||
expect(store.modalVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should hide modal', () => {
|
||||
store.modalHide();
|
||||
expect(store.modalVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should show modal', () => {
|
||||
store.modalHide();
|
||||
expect(store.modalVisible).toBe(false);
|
||||
|
||||
store.modalShow();
|
||||
expect(store.modalVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle modal visibility', () => {
|
||||
expect(store.modalVisible).toBe(true);
|
||||
|
||||
store.modalToggle();
|
||||
expect(store.modalVisible).toBe(false);
|
||||
|
||||
store.modalToggle();
|
||||
expect(store.modalVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
99
web/__test__/store/notifications.test.ts
Normal file
99
web/__test__/store/notifications.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Notifications store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type {
|
||||
NotificationFragmentFragment,
|
||||
NotificationImportance,
|
||||
NotificationType,
|
||||
} from '~/composables/gql/graphql';
|
||||
|
||||
import { useNotificationsStore } from '~/store/notifications';
|
||||
|
||||
describe('Notifications Store', () => {
|
||||
let store: ReturnType<typeof useNotificationsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useNotificationsStore();
|
||||
});
|
||||
|
||||
describe('State and Initialization', () => {
|
||||
it('should initialize with empty notifications array', () => {
|
||||
expect(store.notifications).toEqual([]);
|
||||
});
|
||||
|
||||
it('should initialize with isOpen set to false', () => {
|
||||
expect(store.isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Getters', () => {
|
||||
it('should return correct title based on isOpen state', () => {
|
||||
// Initial state (closed)
|
||||
expect(store.title).toBe('Notifications Are Closed');
|
||||
|
||||
// After opening
|
||||
store.toggle();
|
||||
expect(store.title).toBe('Notifications Are Open');
|
||||
|
||||
// After closing again
|
||||
store.toggle();
|
||||
expect(store.title).toBe('Notifications Are Closed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should toggle isOpen state', () => {
|
||||
// Initial state is false
|
||||
expect(store.isOpen).toBe(false);
|
||||
|
||||
// First toggle - should become true
|
||||
store.toggle();
|
||||
expect(store.isOpen).toBe(true);
|
||||
|
||||
// Second toggle - should become false again
|
||||
store.toggle();
|
||||
expect(store.isOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should set notifications correctly', () => {
|
||||
const mockNotifications: NotificationFragmentFragment[] = [
|
||||
{
|
||||
__typename: 'Notification',
|
||||
id: '1',
|
||||
title: 'Test Notification 1',
|
||||
subject: 'Test Subject 1',
|
||||
description: 'This is a test notification 1',
|
||||
importance: 'NORMAL' as NotificationImportance,
|
||||
type: 'SYSTEM' as NotificationType,
|
||||
timestamp: '2023-01-01T12:00:00Z',
|
||||
formattedTimestamp: 'Jan 1, 2023',
|
||||
},
|
||||
{
|
||||
__typename: 'Notification',
|
||||
id: '2',
|
||||
title: 'Test Notification 2',
|
||||
subject: 'Test Subject 2',
|
||||
description: 'This is a test notification 2',
|
||||
importance: 'HIGH' as NotificationImportance,
|
||||
type: 'UPDATE' as NotificationType,
|
||||
timestamp: '2023-01-02T12:00:00Z',
|
||||
formattedTimestamp: 'Jan 2, 2023',
|
||||
link: 'https://example.com',
|
||||
},
|
||||
];
|
||||
|
||||
store.setNotifications(mockNotifications);
|
||||
|
||||
expect(store.notifications).toEqual(mockNotifications);
|
||||
expect(store.notifications.length).toBe(2);
|
||||
expect(store.notifications[0].id).toBe('1');
|
||||
expect(store.notifications[1].id).toBe('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
176
web/__test__/store/purchase.test.ts
Normal file
176
web/__test__/store/purchase.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Purchase store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { PURCHASE_CALLBACK } from '~/helpers/urls';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
|
||||
// Mock dependencies
|
||||
const mockSend = vi.fn();
|
||||
const mockServerStore = {
|
||||
serverPurchasePayload: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
inIframe: false,
|
||||
};
|
||||
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
send: mockSend,
|
||||
sendType: 'post',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => mockServerStore,
|
||||
}));
|
||||
|
||||
describe('Purchase Store', () => {
|
||||
let store: ReturnType<typeof usePurchaseStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mock values
|
||||
mockServerStore.inIframe = false;
|
||||
|
||||
setActivePinia(createPinia());
|
||||
store = usePurchaseStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should call activate action correctly', () => {
|
||||
store.activate();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'activate',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call redeem action correctly', () => {
|
||||
store.redeem();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'redeem',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call purchase action correctly', () => {
|
||||
store.purchase();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'purchase',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call upgrade action correctly', () => {
|
||||
store.upgrade();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'upgrade',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call renew action correctly', () => {
|
||||
store.renew();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'renew',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'post'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle iframe redirection correctly', () => {
|
||||
// Set up the iframe state
|
||||
mockServerStore.inIframe = true;
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const iframeStore = usePurchaseStore();
|
||||
|
||||
iframeStore.purchase();
|
||||
|
||||
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[
|
||||
{
|
||||
server: {
|
||||
guid: 'test-guid',
|
||||
name: 'test-server',
|
||||
},
|
||||
type: 'purchase',
|
||||
},
|
||||
],
|
||||
'newTab',
|
||||
'post'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
334
web/__test__/store/replaceRenew.test.ts
Normal file
334
web/__test__/store/replaceRenew.test.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* ReplaceRenew store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ValidateGuidResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { validateGuid } from '~/composables/services/keyServer';
|
||||
import { REPLACE_CHECK_LOCAL_STORAGE_KEY, useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
vi.mock('@unraid/shared-callbacks', () => ({}));
|
||||
|
||||
vi.mock('~/composables/services/keyServer', () => ({
|
||||
validateGuid: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockSessionStorage = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'sessionStorage', { value: mockSessionStorage });
|
||||
|
||||
describe('ReplaceRenew Store', () => {
|
||||
let store: ReturnType<typeof useReplaceRenewStore>;
|
||||
let mockGuid = 'test-guid';
|
||||
let mockKeyfile = 'test-keyfile.key';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
mockGuid = 'test-guid';
|
||||
mockKeyfile = 'test-keyfile.key';
|
||||
|
||||
vi.mocked(useServerStore).mockReturnValue({
|
||||
guid: mockGuid,
|
||||
keyfile: mockKeyfile,
|
||||
} as unknown as ReturnType<typeof useServerStore>);
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
setActivePinia(createPinia());
|
||||
store = useReplaceRenewStore();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Initialization', () => {
|
||||
it('should initialize with default state values', () => {
|
||||
expect(store.keyLinkedStatus).toBe('ready');
|
||||
expect(store.renewStatus).toBe('ready');
|
||||
expect(store.replaceStatus).toBe('ready');
|
||||
});
|
||||
|
||||
it('should initialize with error state when guid is missing', () => {
|
||||
vi.mocked(useServerStore).mockReturnValueOnce({
|
||||
guid: undefined,
|
||||
keyfile: mockKeyfile,
|
||||
} as unknown as ReturnType<typeof useServerStore>);
|
||||
|
||||
setActivePinia(createPinia());
|
||||
|
||||
const newStore = useReplaceRenewStore();
|
||||
|
||||
expect(newStore.replaceStatus).toBe('error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed Properties', () => {
|
||||
it('should return correct keyLinkedOutput for each status', () => {
|
||||
expect(store.keyLinkedOutput.variant).toBe('gray');
|
||||
expect(store.keyLinkedOutput.text).toBe('Unknown');
|
||||
|
||||
store.keyLinkedStatus = 'checking';
|
||||
expect(store.keyLinkedOutput.variant).toBe('gray');
|
||||
expect(store.keyLinkedOutput.text).toBe('Checking...');
|
||||
|
||||
store.keyLinkedStatus = 'linked';
|
||||
expect(store.keyLinkedOutput.variant).toBe('green');
|
||||
expect(store.keyLinkedOutput.text).toBe('Linked');
|
||||
|
||||
store.keyLinkedStatus = 'notLinked';
|
||||
expect(store.keyLinkedOutput.variant).toBe('yellow');
|
||||
expect(store.keyLinkedOutput.text).toBe('Not Linked');
|
||||
|
||||
store.keyLinkedStatus = 'error';
|
||||
// Test with a specific error message
|
||||
store.error = { name: 'TestError', message: 'Specific Linked Error' };
|
||||
expect(store.keyLinkedOutput.variant).toBe('red');
|
||||
expect(store.keyLinkedOutput.text).toBe('Specific Linked Error');
|
||||
});
|
||||
|
||||
it('should return correct replaceStatusOutput for each status', () => {
|
||||
expect(store.replaceStatusOutput).toBeUndefined();
|
||||
|
||||
store.replaceStatus = 'checking';
|
||||
expect(store.replaceStatusOutput?.variant).toBe('gray');
|
||||
expect(store.replaceStatusOutput?.text).toBe('Checking...');
|
||||
|
||||
store.replaceStatus = 'eligible';
|
||||
expect(store.replaceStatusOutput?.variant).toBe('green');
|
||||
expect(store.replaceStatusOutput?.text).toBe('Eligible');
|
||||
|
||||
store.replaceStatus = 'ineligible';
|
||||
expect(store.replaceStatusOutput?.variant).toBe('red');
|
||||
expect(store.replaceStatusOutput?.text).toBe('Ineligible for self-replacement');
|
||||
|
||||
store.replaceStatus = 'error';
|
||||
store.error = { name: 'TestError', message: 'Specific Replace Error' };
|
||||
|
||||
expect(store.replaceStatusOutput?.variant).toBe('red');
|
||||
expect(store.replaceStatusOutput?.text).toBe('Specific Replace Error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should purge validation response', async () => {
|
||||
await store.purgeValidationResponse();
|
||||
|
||||
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(REPLACE_CHECK_LOCAL_STORAGE_KEY);
|
||||
});
|
||||
|
||||
it('should set status actions correctly', () => {
|
||||
store.setReplaceStatus('eligible');
|
||||
expect(store.replaceStatus).toBe('eligible');
|
||||
|
||||
store.setRenewStatus('installing');
|
||||
expect(store.renewStatus).toBe('installing');
|
||||
});
|
||||
|
||||
describe('check action', () => {
|
||||
const mockResponse = {
|
||||
hasNewerKeyfile: false,
|
||||
linked: true,
|
||||
replaceable: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(validateGuid).mockResolvedValue(mockResponse as unknown as ValidateGuidResponse);
|
||||
mockSessionStorage.getItem.mockReturnValue(null);
|
||||
vi.mocked(useServerStore).mockReturnValue({
|
||||
guid: 'test-guid',
|
||||
keyfile: 'test-keyfile.key',
|
||||
} as unknown as ReturnType<typeof useServerStore>);
|
||||
|
||||
setActivePinia(createPinia());
|
||||
store = useReplaceRenewStore();
|
||||
});
|
||||
|
||||
it('should handle missing guid', async () => {
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
testStore.setReplaceStatus('error');
|
||||
|
||||
expect(testStore.replaceStatus).toBe('error');
|
||||
});
|
||||
|
||||
it('should handle missing keyfile', async () => {
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
testStore.setReplaceStatus('error');
|
||||
|
||||
expect(testStore.replaceStatus).toBe('error');
|
||||
});
|
||||
|
||||
it('should use cached response if available and valid', async () => {
|
||||
const cachedResponse = {
|
||||
key: 'eyfile.key',
|
||||
timestamp: Date.now(),
|
||||
hasNewerKeyfile: false,
|
||||
linked: false,
|
||||
replaceable: false,
|
||||
};
|
||||
|
||||
mockSessionStorage.getItem.mockReturnValue(JSON.stringify(cachedResponse));
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
await testStore.check();
|
||||
|
||||
expect(validateGuid).not.toHaveBeenCalled();
|
||||
|
||||
expect(testStore.keyLinkedStatus).toBe('notLinked');
|
||||
expect(testStore.replaceStatus).toBe('ineligible');
|
||||
|
||||
expect(mockSessionStorage.removeItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should purge cache and re-fetch if timestamp is expired', async () => {
|
||||
vi.useFakeTimers();
|
||||
const expiredTimestamp = Date.now() - 8 * 24 * 60 * 60 * 1000;
|
||||
const cachedResponse = {
|
||||
key: 'eyfile.key',
|
||||
timestamp: expiredTimestamp,
|
||||
hasNewerKeyfile: false,
|
||||
linked: false,
|
||||
replaceable: false,
|
||||
};
|
||||
|
||||
mockSessionStorage.getItem.mockReturnValue(JSON.stringify(cachedResponse));
|
||||
vi.mocked(validateGuid).mockResolvedValue(mockResponse as unknown as ValidateGuidResponse);
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
await testStore.check();
|
||||
|
||||
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(REPLACE_CHECK_LOCAL_STORAGE_KEY);
|
||||
|
||||
expect(validateGuid).toHaveBeenCalled();
|
||||
expect(testStore.keyLinkedStatus).toBe('linked');
|
||||
expect(testStore.replaceStatus).toBe('eligible');
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should purge cache and re-fetch if key is missing in cache', async () => {
|
||||
const cachedResponse = {
|
||||
timestamp: Date.now(),
|
||||
hasNewerKeyfile: false,
|
||||
linked: false,
|
||||
replaceable: false,
|
||||
};
|
||||
|
||||
mockSessionStorage.getItem.mockReturnValue(JSON.stringify(cachedResponse));
|
||||
vi.mocked(validateGuid).mockResolvedValue(mockResponse as unknown as ValidateGuidResponse);
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
await testStore.check();
|
||||
|
||||
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(REPLACE_CHECK_LOCAL_STORAGE_KEY);
|
||||
expect(validateGuid).toHaveBeenCalled();
|
||||
expect(testStore.keyLinkedStatus).toBe('linked');
|
||||
expect(testStore.replaceStatus).toBe('eligible');
|
||||
});
|
||||
|
||||
it('should purge cache and re-fetch if key in cache mismatches current keyfile', async () => {
|
||||
const cachedResponse = {
|
||||
key: 'mismatched',
|
||||
timestamp: Date.now(),
|
||||
hasNewerKeyfile: false,
|
||||
linked: false,
|
||||
replaceable: false,
|
||||
};
|
||||
|
||||
mockSessionStorage.getItem.mockReturnValue(JSON.stringify(cachedResponse));
|
||||
vi.mocked(validateGuid).mockResolvedValue(mockResponse as unknown as ValidateGuidResponse);
|
||||
setActivePinia(createPinia());
|
||||
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
await testStore.check();
|
||||
|
||||
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(REPLACE_CHECK_LOCAL_STORAGE_KEY);
|
||||
expect(validateGuid).toHaveBeenCalled();
|
||||
expect(testStore.keyLinkedStatus).toBe('linked');
|
||||
expect(testStore.replaceStatus).toBe('eligible');
|
||||
});
|
||||
|
||||
it('should call validateGuid with correct parameters', async () => {
|
||||
await store.check();
|
||||
|
||||
expect(validateGuid).toHaveBeenCalledWith({
|
||||
guid: 'test-guid',
|
||||
keyfile: 'test-keyfile.key',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set statuses based on validateGuid response', async () => {
|
||||
await store.check();
|
||||
|
||||
expect(store.keyLinkedStatus).toBe('linked');
|
||||
expect(store.replaceStatus).toBe('eligible');
|
||||
});
|
||||
|
||||
it('should cache the validation response', async () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date('2023-01-01').getTime();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
await store.check();
|
||||
|
||||
expect(mockSessionStorage.setItem).toHaveBeenCalledWith(
|
||||
REPLACE_CHECK_LOCAL_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
key: 'eyfile.key',
|
||||
timestamp: now,
|
||||
...mockResponse,
|
||||
})
|
||||
);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should purge cache when skipCache is true', async () => {
|
||||
mockSessionStorage.getItem.mockReturnValue(
|
||||
JSON.stringify({ key: 'eyfile.key', timestamp: Date.now() })
|
||||
);
|
||||
setActivePinia(createPinia());
|
||||
const testStore = useReplaceRenewStore();
|
||||
|
||||
await testStore.check(true);
|
||||
|
||||
expect(mockSessionStorage.removeItem).toHaveBeenCalledWith(REPLACE_CHECK_LOCAL_STORAGE_KEY);
|
||||
expect(validateGuid).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors during check', async () => {
|
||||
const testError = new Error('Test error');
|
||||
vi.mocked(validateGuid).mockRejectedValueOnce(testError);
|
||||
|
||||
await store.check();
|
||||
|
||||
expect(store.replaceStatus).toBe('error');
|
||||
expect(console.error).toHaveBeenCalledWith('[ReplaceCheck.check]', testError);
|
||||
expect(store.error).toEqual(testError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
166
web/__test__/store/theme.test.ts
Normal file
166
web/__test__/store/theme.test.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Theme store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { defaultColors } from '~/themes/default';
|
||||
import hexToRgba from 'hex-to-rgba';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
vi.mock('hex-to-rgba', () => ({
|
||||
default: vi.fn((hex, opacity) => `rgba(mock-${hex}-${opacity})`),
|
||||
}));
|
||||
|
||||
describe('Theme Store', () => {
|
||||
let store: ReturnType<typeof useThemeStore>;
|
||||
const originalAddClassFn = document.body.classList.add;
|
||||
const originalRemoveClassFn = document.body.classList.remove;
|
||||
const originalStyleCssText = document.body.style.cssText;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useThemeStore();
|
||||
|
||||
document.body.classList.add = vi.fn();
|
||||
document.body.classList.remove = vi.fn();
|
||||
document.body.style.cssText = '';
|
||||
|
||||
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
cb(0);
|
||||
return 0;
|
||||
});
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original methods
|
||||
document.body.classList.add = originalAddClassFn;
|
||||
document.body.classList.remove = originalRemoveClassFn;
|
||||
document.body.style.cssText = originalStyleCssText;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Initialization', () => {
|
||||
it('should initialize with default theme', () => {
|
||||
expect(store.theme).toEqual({
|
||||
name: 'white',
|
||||
banner: false,
|
||||
bannerGradient: false,
|
||||
bgColor: '',
|
||||
descriptionShow: false,
|
||||
metaColor: '',
|
||||
textColor: '',
|
||||
});
|
||||
expect(store.activeColorVariables).toEqual(defaultColors.white);
|
||||
});
|
||||
|
||||
it('should compute darkMode correctly', () => {
|
||||
expect(store.darkMode).toBe(false);
|
||||
|
||||
store.setTheme({ ...store.theme, name: 'black' });
|
||||
expect(store.darkMode).toBe(true);
|
||||
|
||||
store.setTheme({ ...store.theme, name: 'gray' });
|
||||
expect(store.darkMode).toBe(true);
|
||||
|
||||
store.setTheme({ ...store.theme, name: 'white' });
|
||||
expect(store.darkMode).toBe(false);
|
||||
});
|
||||
|
||||
it('should compute bannerGradient correctly', () => {
|
||||
expect(store.bannerGradient).toBeUndefined();
|
||||
|
||||
store.setTheme({
|
||||
...store.theme,
|
||||
banner: true,
|
||||
bannerGradient: true,
|
||||
});
|
||||
expect(store.bannerGradient).toBe(
|
||||
'background-image: linear-gradient(90deg, rgba(0, 0, 0, 0) 0, var(--header-background-color) 30%);'
|
||||
);
|
||||
|
||||
store.setTheme({
|
||||
...store.theme,
|
||||
banner: true,
|
||||
bannerGradient: true,
|
||||
bgColor: '#123456',
|
||||
});
|
||||
expect(store.bannerGradient).toBe(
|
||||
'background-image: linear-gradient(90deg, var(--header-gradient-start) 0, var(--header-gradient-end) 30%);'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should set theme correctly', () => {
|
||||
const newTheme = {
|
||||
name: 'black',
|
||||
banner: true,
|
||||
bannerGradient: true,
|
||||
bgColor: '#123456',
|
||||
descriptionShow: true,
|
||||
metaColor: '#abcdef',
|
||||
textColor: '#ffffff',
|
||||
};
|
||||
|
||||
store.setTheme(newTheme);
|
||||
expect(store.theme).toEqual(newTheme);
|
||||
});
|
||||
|
||||
it('should update body classes for dark mode', async () => {
|
||||
store.setTheme({ ...store.theme, name: 'black' });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(document.body.classList.add).toHaveBeenCalledWith('dark');
|
||||
|
||||
store.setTheme({ ...store.theme, name: 'white' });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(document.body.classList.remove).toHaveBeenCalledWith('dark');
|
||||
});
|
||||
|
||||
it('should update activeColorVariables when theme changes', async () => {
|
||||
store.setTheme({
|
||||
...store.theme,
|
||||
name: 'white',
|
||||
textColor: '#333333',
|
||||
metaColor: '#666666',
|
||||
bgColor: '#ffffff',
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.activeColorVariables['--header-text-primary']).toBe('#333333');
|
||||
expect(store.activeColorVariables['--header-text-secondary']).toBe('#666666');
|
||||
expect(store.activeColorVariables['--header-background-color']).toBe('#ffffff');
|
||||
});
|
||||
|
||||
it('should handle banner gradient correctly', async () => {
|
||||
const mockHexToRgba = vi.mocked(hexToRgba);
|
||||
|
||||
mockHexToRgba.mockClear();
|
||||
|
||||
store.setTheme({
|
||||
...store.theme,
|
||||
banner: true,
|
||||
bannerGradient: true,
|
||||
bgColor: '#112233',
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockHexToRgba).toHaveBeenCalledWith('#112233', 0);
|
||||
expect(mockHexToRgba).toHaveBeenCalledWith('#112233', 0.7);
|
||||
|
||||
expect(store.activeColorVariables['--header-gradient-start']).toBe('rgba(mock-#112233-0)');
|
||||
expect(store.activeColorVariables['--header-gradient-end']).toBe('rgba(mock-#112233-0.7)');
|
||||
});
|
||||
});
|
||||
});
|
||||
215
web/__test__/store/trial.test.ts
Normal file
215
web/__test__/store/trial.test.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Trial store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { StartTrialResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
|
||||
const mockAddPreventClose = vi.fn();
|
||||
const mockRemovePreventClose = vi.fn();
|
||||
const mockStartTrial = vi.fn();
|
||||
const mockSaveCallbackData = vi.fn();
|
||||
const mockDropdownHide = vi.fn();
|
||||
|
||||
vi.mock('~/composables/preventClose', () => ({
|
||||
addPreventClose: () => mockAddPreventClose(),
|
||||
removePreventClose: () => mockRemovePreventClose(),
|
||||
}));
|
||||
|
||||
vi.mock('~/composables/services/keyServer', () => ({
|
||||
startTrial: (payload: unknown) => mockStartTrial(payload),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
saveCallbackData: mockSaveCallbackData,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/dropdown', () => ({
|
||||
useDropdownStore: () => ({
|
||||
dropdownHide: mockDropdownHide,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => ({
|
||||
guid: 'test-guid',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Trial Store', () => {
|
||||
let store: ReturnType<typeof useTrialStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useTrialStore();
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
|
||||
mockStartTrial.mockResolvedValue({
|
||||
license: 'mock-license-key',
|
||||
});
|
||||
|
||||
// Suppress console output during tests
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('State and Getters', () => {
|
||||
it('should initialize with ready status', () => {
|
||||
expect(store.trialStatus).toBe('ready');
|
||||
expect(store.trialModalLoading).toBe(false);
|
||||
expect(store.trialModalVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should update trialModalLoading when status is trialExtend or trialStart', () => {
|
||||
store.setTrialStatus('ready');
|
||||
expect(store.trialModalLoading).toBe(false);
|
||||
|
||||
store.setTrialStatus('trialStart');
|
||||
expect(store.trialModalLoading).toBe(true);
|
||||
|
||||
store.setTrialStatus('trialExtend');
|
||||
expect(store.trialModalLoading).toBe(true);
|
||||
});
|
||||
|
||||
it('should update trialModalVisible when status is trialExtend, trialStart, or failed', () => {
|
||||
store.setTrialStatus('ready');
|
||||
expect(store.trialModalVisible).toBe(false);
|
||||
|
||||
store.setTrialStatus('trialStart');
|
||||
expect(store.trialModalVisible).toBe(true);
|
||||
|
||||
store.setTrialStatus('trialExtend');
|
||||
expect(store.trialModalVisible).toBe(true);
|
||||
|
||||
store.setTrialStatus('failed');
|
||||
expect(store.trialModalVisible).toBe(true);
|
||||
|
||||
store.setTrialStatus('success');
|
||||
expect(store.trialModalVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should set trial status', () => {
|
||||
store.setTrialStatus('trialStart');
|
||||
expect(store.trialStatus).toBe('trialStart');
|
||||
|
||||
store.setTrialStatus('failed');
|
||||
expect(store.trialStatus).toBe('failed');
|
||||
});
|
||||
|
||||
it('should call addPreventClose and dropdownHide when status changes to trialStart', async () => {
|
||||
const watchSpy = vi.spyOn(global, 'setTimeout');
|
||||
|
||||
store.setTrialStatus('trialStart');
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockAddPreventClose).toHaveBeenCalledTimes(1);
|
||||
expect(mockDropdownHide).toHaveBeenCalledTimes(1);
|
||||
expect(watchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(watchSpy).toHaveBeenLastCalledWith(expect.any(Function), 1500);
|
||||
});
|
||||
|
||||
it('should call addPreventClose and dropdownHide when status changes to trialExtend', async () => {
|
||||
const watchSpy = vi.spyOn(global, 'setTimeout');
|
||||
|
||||
store.setTrialStatus('trialExtend');
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockAddPreventClose).toHaveBeenCalledTimes(1);
|
||||
expect(mockDropdownHide).toHaveBeenCalledTimes(1);
|
||||
expect(watchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(watchSpy).toHaveBeenLastCalledWith(expect.any(Function), 1500);
|
||||
});
|
||||
|
||||
it('should call removePreventClose when status changes to failed', async () => {
|
||||
store.setTrialStatus('failed');
|
||||
await nextTick();
|
||||
|
||||
expect(mockRemovePreventClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call removePreventClose when status changes to success', async () => {
|
||||
store.setTrialStatus('success');
|
||||
await nextTick();
|
||||
|
||||
expect(mockRemovePreventClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should request trial after delay when status is trialStart', async () => {
|
||||
store.setTrialStatus('trialStart');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(mockStartTrial).toHaveBeenCalledWith({
|
||||
guid: 'test-guid',
|
||||
timestamp: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it('should request trial after delay when status is trialExtend', async () => {
|
||||
store.setTrialStatus('trialExtend');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
expect(mockStartTrial).toHaveBeenCalledWith({
|
||||
guid: 'test-guid',
|
||||
timestamp: expect.any(Number),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Trial Request', () => {
|
||||
it('should handle successful trial request', async () => {
|
||||
await store.requestTrial('trialStart');
|
||||
|
||||
expect(mockStartTrial).toHaveBeenCalledWith({
|
||||
guid: 'test-guid',
|
||||
timestamp: expect.any(Number),
|
||||
});
|
||||
|
||||
expect(mockSaveCallbackData).toHaveBeenCalledWith({
|
||||
actions: [
|
||||
{
|
||||
keyUrl: 'mock-license-key',
|
||||
type: 'trialStart',
|
||||
},
|
||||
],
|
||||
sender: expect.any(String),
|
||||
type: 'forUpc',
|
||||
});
|
||||
|
||||
expect(store.trialStatus).toBe('success');
|
||||
});
|
||||
|
||||
it('should set failed status when no license is returned', async () => {
|
||||
mockStartTrial.mockResolvedValueOnce({
|
||||
license: undefined,
|
||||
} as StartTrialResponse);
|
||||
|
||||
await store.requestTrial('trialStart');
|
||||
|
||||
expect(store.trialStatus).toBe('failed');
|
||||
expect(mockSaveCallbackData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set failed status when an error occurs', async () => {
|
||||
mockStartTrial.mockRejectedValueOnce(new Error('API error'));
|
||||
|
||||
await store.requestTrial('trialStart');
|
||||
|
||||
expect(store.trialStatus).toBe('failed');
|
||||
expect(mockSaveCallbackData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
271
web/__test__/store/unraidApi.test.ts
Normal file
271
web/__test__/store/unraidApi.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* UnraidApi store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
|
||||
vi.mock('~/helpers/create-apollo-client', () => ({
|
||||
client: {
|
||||
clearStore: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('~/composables/services/webgui', () => ({
|
||||
WebguiUnraidApiCommand: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const mockErrorsStore = {
|
||||
setError: vi.fn(),
|
||||
removeErrorByRef: vi.fn(),
|
||||
};
|
||||
|
||||
const mockServerStore = {
|
||||
connectPluginInstalled: true,
|
||||
stateDataError: false,
|
||||
csrf: 'mock-csrf-token',
|
||||
};
|
||||
|
||||
describe('UnraidApi Store', () => {
|
||||
vi.mock('~/store/errors', () => ({
|
||||
useErrorsStore: () => mockErrorsStore,
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => mockServerStore,
|
||||
}));
|
||||
|
||||
let store: ReturnType<typeof useUnraidApiStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
Object.assign(mockErrorsStore, {
|
||||
setError: vi.fn(),
|
||||
removeErrorByRef: vi.fn(),
|
||||
});
|
||||
|
||||
Object.assign(mockServerStore, {
|
||||
connectPluginInstalled: true,
|
||||
stateDataError: false,
|
||||
csrf: 'mock-csrf-token',
|
||||
});
|
||||
|
||||
setActivePinia(createPinia());
|
||||
|
||||
store = useUnraidApiStore();
|
||||
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should initialize with correct default values', () => {
|
||||
expect(store.unraidApiStatus).toBe('connecting');
|
||||
expect(store.prioritizeCorsError).toBe(false);
|
||||
expect(store.unraidApiClient).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Computed properties', () => {
|
||||
it('should have offlineError computed when status is offline', () => {
|
||||
expect(store.offlineError).toBeUndefined();
|
||||
|
||||
store.unraidApiStatus = 'offline';
|
||||
expect(store.offlineError).toBeInstanceOf(Error);
|
||||
expect(store.offlineError?.message).toBe('The Unraid API is currently offline.');
|
||||
});
|
||||
|
||||
it('should have unraidApiRestartAction when status is offline', () => {
|
||||
expect(store.unraidApiRestartAction).toBeUndefined();
|
||||
|
||||
store.unraidApiStatus = 'offline';
|
||||
expect(store.unraidApiRestartAction).toBeDefined();
|
||||
expect(store.unraidApiRestartAction?.text).toBe('Restart unraid-api');
|
||||
expect(typeof store.unraidApiRestartAction?.click).toBe('function');
|
||||
});
|
||||
|
||||
it('should not provide restart action when plugin not installed', () => {
|
||||
store.unraidApiStatus = 'offline';
|
||||
mockServerStore.connectPluginInstalled = false;
|
||||
|
||||
store = useUnraidApiStore();
|
||||
|
||||
expect(store.unraidApiRestartAction).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not provide restart action when stateDataError exists', () => {
|
||||
store.unraidApiStatus = 'offline';
|
||||
mockServerStore.stateDataError = true;
|
||||
|
||||
store = useUnraidApiStore();
|
||||
|
||||
expect(store.unraidApiRestartAction).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have restart action with click function when status is offline', () => {
|
||||
store.unraidApiStatus = 'offline';
|
||||
|
||||
const action = store.unraidApiRestartAction;
|
||||
expect(action).toBeDefined();
|
||||
expect(action?.text).toBe('Restart unraid-api');
|
||||
expect(typeof action?.click).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Watchers', () => {
|
||||
it('should set error when status changes to offline', async () => {
|
||||
store.unraidApiStatus = 'offline';
|
||||
await nextTick();
|
||||
|
||||
expect(mockErrorsStore.setError).toHaveBeenCalledWith({
|
||||
heading: 'Warning: API is offline!',
|
||||
message: 'The Unraid API is currently offline.',
|
||||
ref: 'unraidApiOffline',
|
||||
level: 'warning',
|
||||
type: 'unraidApiState',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove error when status changes from offline', async () => {
|
||||
store.unraidApiStatus = 'offline';
|
||||
await nextTick();
|
||||
|
||||
store.unraidApiStatus = 'online';
|
||||
await nextTick();
|
||||
|
||||
expect(mockErrorsStore.removeErrorByRef).toHaveBeenCalledWith('unraidApiOffline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Client actions', () => {
|
||||
it('should close client correctly', async () => {
|
||||
if (store.unraidApiClient) {
|
||||
const clearStoreSpy = vi.fn().mockResolvedValue(undefined);
|
||||
const stopSpy = vi.fn();
|
||||
|
||||
const originalClearStore = store.unraidApiClient.clearStore;
|
||||
const originalStop = store.unraidApiClient.stop;
|
||||
|
||||
store.unraidApiClient.clearStore = clearStoreSpy;
|
||||
store.unraidApiClient.stop = stopSpy;
|
||||
|
||||
try {
|
||||
await store.closeUnraidApiClient();
|
||||
|
||||
expect(clearStoreSpy).toHaveBeenCalledTimes(1);
|
||||
expect(stopSpy).toHaveBeenCalledTimes(1);
|
||||
expect(store.unraidApiClient).toBeNull();
|
||||
expect(store.unraidApiStatus).toBe('offline');
|
||||
} finally {
|
||||
if (store.unraidApiClient) {
|
||||
store.unraidApiClient.clearStore = originalClearStore;
|
||||
store.unraidApiClient.stop = originalStop;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle null client during close', async () => {
|
||||
store.unraidApiClient = null;
|
||||
await store.closeUnraidApiClient();
|
||||
|
||||
expect(store.unraidApiClient).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Restart actions', () => {
|
||||
it('should restart client when status is offline', async () => {
|
||||
const { WebguiUnraidApiCommand } = await import('~/composables/services/webgui');
|
||||
const mockWebguiCommand = vi.mocked(WebguiUnraidApiCommand);
|
||||
|
||||
store.unraidApiStatus = 'offline';
|
||||
await store.restartUnraidApiClient();
|
||||
|
||||
expect(mockWebguiCommand).toHaveBeenCalledWith({
|
||||
csrf_token: 'mock-csrf-token',
|
||||
command: 'start',
|
||||
});
|
||||
expect(store.unraidApiStatus).toBe('restarting');
|
||||
});
|
||||
|
||||
it('should restart client when status is online', async () => {
|
||||
const { WebguiUnraidApiCommand } = await import('~/composables/services/webgui');
|
||||
const mockWebguiCommand = vi.mocked(WebguiUnraidApiCommand);
|
||||
|
||||
store.unraidApiStatus = 'online';
|
||||
await store.restartUnraidApiClient();
|
||||
|
||||
expect(mockWebguiCommand).toHaveBeenCalledWith({
|
||||
csrf_token: 'mock-csrf-token',
|
||||
command: 'restart',
|
||||
});
|
||||
expect(store.unraidApiStatus).toBe('restarting');
|
||||
});
|
||||
|
||||
it('should handle error during restart', async () => {
|
||||
const { WebguiUnraidApiCommand } = await import('~/composables/services/webgui');
|
||||
const mockWebguiCommand = vi.mocked(WebguiUnraidApiCommand);
|
||||
|
||||
mockWebguiCommand.mockRejectedValueOnce(new Error('API restart failed'));
|
||||
|
||||
store.unraidApiStatus = 'online';
|
||||
await store.restartUnraidApiClient();
|
||||
|
||||
expect(mockWebguiCommand).toHaveBeenCalled();
|
||||
expect(mockErrorsStore.setError).toHaveBeenCalledWith({
|
||||
heading: 'Error: unraid-api restart',
|
||||
message: 'API restart failed',
|
||||
level: 'error',
|
||||
ref: 'restartUnraidApiClient',
|
||||
type: 'request',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle string error during restart', async () => {
|
||||
const { WebguiUnraidApiCommand } = await import('~/composables/services/webgui');
|
||||
const mockWebguiCommand = vi.mocked(WebguiUnraidApiCommand);
|
||||
|
||||
mockWebguiCommand.mockRejectedValueOnce('string error');
|
||||
|
||||
store.unraidApiStatus = 'online';
|
||||
await store.restartUnraidApiClient();
|
||||
|
||||
expect(mockWebguiCommand).toHaveBeenCalled();
|
||||
expect(mockErrorsStore.setError).toHaveBeenCalledWith({
|
||||
heading: 'Error: unraid-api restart',
|
||||
message: 'STRING ERROR',
|
||||
level: 'error',
|
||||
ref: 'restartUnraidApiClient',
|
||||
type: 'request',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown error during restart', async () => {
|
||||
const { WebguiUnraidApiCommand } = await import('~/composables/services/webgui');
|
||||
const mockWebguiCommand = vi.mocked(WebguiUnraidApiCommand);
|
||||
|
||||
mockWebguiCommand.mockRejectedValueOnce(null);
|
||||
|
||||
store.unraidApiStatus = 'online';
|
||||
await store.restartUnraidApiClient();
|
||||
|
||||
expect(mockWebguiCommand).toHaveBeenCalled();
|
||||
expect(mockErrorsStore.setError).toHaveBeenCalledWith({
|
||||
heading: 'Error: unraid-api restart',
|
||||
message: 'Unknown error',
|
||||
level: 'error',
|
||||
ref: 'restartUnraidApiClient',
|
||||
type: 'request',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
111
web/__test__/store/unraidApiSettings.test.ts
Normal file
111
web/__test__/store/unraidApiSettings.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* UnraidApiSettings store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { WanAccessType, WanForwardType } from '~/composables/gql/graphql';
|
||||
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
|
||||
|
||||
const mockOrigins = ['http://example.com'];
|
||||
const mockRemoteAccess = {
|
||||
accessType: WanAccessType.ALWAYS,
|
||||
forwardType: WanForwardType.UPNP,
|
||||
port: 8080,
|
||||
};
|
||||
|
||||
const mockLoadFn = vi.fn();
|
||||
const mockMutateFn = vi.fn();
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useLazyQuery: () => ({
|
||||
load: mockLoadFn,
|
||||
result: {
|
||||
value: {
|
||||
extraAllowedOrigins: mockOrigins,
|
||||
remoteAccess: mockRemoteAccess,
|
||||
},
|
||||
},
|
||||
}),
|
||||
useMutation: () => ({
|
||||
mutate: mockMutateFn.mockImplementation((args) => {
|
||||
if (args?.input?.origins) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
setAdditionalAllowedOrigins: args.input.origins,
|
||||
},
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
setupRemoteAccess: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('UnraidApiSettings Store', () => {
|
||||
let store: ReturnType<typeof useUnraidApiSettingsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useUnraidApiSettingsStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getAllowedOrigins', () => {
|
||||
it('should get origins successfully', async () => {
|
||||
const origins = await store.getAllowedOrigins();
|
||||
|
||||
expect(mockLoadFn).toHaveBeenCalled();
|
||||
expect(Array.isArray(origins)).toBe(true);
|
||||
expect(origins).toEqual(mockOrigins);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAllowedOrigins', () => {
|
||||
it('should set origins and return the updated list of allowed origins', async () => {
|
||||
const newOrigins = ['http://example.com', 'http://test.com'];
|
||||
const result = await store.setAllowedOrigins(newOrigins);
|
||||
|
||||
expect(mockMutateFn).toHaveBeenCalledWith({
|
||||
input: { origins: newOrigins },
|
||||
});
|
||||
expect(result).toEqual(newOrigins);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRemoteAccess', () => {
|
||||
it('should get remote access configuration successfully', async () => {
|
||||
const result = await store.getRemoteAccess();
|
||||
|
||||
expect(mockLoadFn).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
|
||||
if (result) {
|
||||
expect(result).toEqual(mockRemoteAccess);
|
||||
expect(result.accessType).toBe(WanAccessType.ALWAYS);
|
||||
expect(result.forwardType).toBe(WanForwardType.UPNP);
|
||||
expect(result.port).toBe(8080);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupRemoteAccess', () => {
|
||||
it('should setup remote access successfully and return true', async () => {
|
||||
const input = {
|
||||
accessType: WanAccessType.ALWAYS,
|
||||
forwardType: WanForwardType.STATIC,
|
||||
port: 9090,
|
||||
};
|
||||
|
||||
const result = await store.setupRemoteAccess(input);
|
||||
|
||||
expect(mockMutateFn).toHaveBeenCalledWith({ input });
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
193
web/__test__/store/updateOs.test.ts
Normal file
193
web/__test__/store/updateOs.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* UpdateOs store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
vi.mock('~/composables/services/webgui', () => {
|
||||
return {
|
||||
WebguiCheckForUpdate: vi.fn().mockResolvedValue({
|
||||
version: '6.12.0',
|
||||
name: 'Unraid 6.12.0',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
date: '2023-01-01',
|
||||
sha256: 'test-sha256',
|
||||
changelog: 'https://example.com/changelog',
|
||||
}),
|
||||
WebguiUpdateCancel: vi.fn().mockResolvedValue({ success: true }),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('~/store/server', () => {
|
||||
return {
|
||||
useServerStore: () => ({
|
||||
regExp: '2025-01-01',
|
||||
regUpdatesExpired: false,
|
||||
updateOsResponse: {
|
||||
version: '6.12.0',
|
||||
name: 'Unraid 6.12.0',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
date: '2023-01-01',
|
||||
sha256: 'test-sha256',
|
||||
changelog: 'https://example.com/changelog',
|
||||
},
|
||||
updateOsIgnoredReleases: [],
|
||||
setUpdateOsResponse: vi.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock console.debug to prevent noise in tests
|
||||
console.debug = vi.fn();
|
||||
|
||||
describe('UpdateOs Store', () => {
|
||||
let store: ReturnType<typeof useUpdateOsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useUpdateOsStore();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('State and Getters', () => {
|
||||
it('should initialize with correct default values', () => {
|
||||
expect(store.checkForUpdatesLoading).toBe(false);
|
||||
expect(store.modalOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should have computed properties with the right types', () => {
|
||||
// Test that properties exist with the right types
|
||||
expect(typeof store.available).not.toBe('undefined');
|
||||
expect(typeof store.availableRequiresAuth).toBe('boolean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actions', () => {
|
||||
it('should check for updates and update state', async () => {
|
||||
const { WebguiCheckForUpdate } = await import('~/composables/services/webgui');
|
||||
|
||||
// Mock the method to avoid the error
|
||||
vi.mocked(WebguiCheckForUpdate).mockResolvedValueOnce({
|
||||
version: '6.12.0',
|
||||
name: 'Unraid 6.12.0',
|
||||
isNewer: true,
|
||||
isEligible: true,
|
||||
date: '2023-01-01',
|
||||
sha256: 'test-sha256',
|
||||
changelog: 'https://example.com/changelog',
|
||||
});
|
||||
|
||||
await store.localCheckForUpdate();
|
||||
|
||||
expect(WebguiCheckForUpdate).toHaveBeenCalled();
|
||||
expect(store.modalOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('should set modal open state', () => {
|
||||
store.setModalOpen(true);
|
||||
expect(store.modalOpen).toBe(true);
|
||||
|
||||
store.setModalOpen(false);
|
||||
expect(store.modalOpen).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors when checking for updates', async () => {
|
||||
const { WebguiCheckForUpdate } = await import('~/composables/services/webgui');
|
||||
|
||||
vi.mocked(WebguiCheckForUpdate).mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
await expect(store.localCheckForUpdate()).rejects.toThrow(
|
||||
'[localCheckForUpdate] Error checking for updates'
|
||||
);
|
||||
|
||||
expect(WebguiCheckForUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should successfully cancel an update', async () => {
|
||||
const { WebguiUpdateCancel } = await import('~/composables/services/webgui');
|
||||
const originalLocation = window.location;
|
||||
const mockReload = vi.fn();
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: {
|
||||
...originalLocation,
|
||||
pathname: '/some/other/path',
|
||||
reload: mockReload,
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(WebguiUpdateCancel).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await store.cancelUpdate();
|
||||
|
||||
expect(WebguiUpdateCancel).toHaveBeenCalled();
|
||||
expect(mockReload).toHaveBeenCalled();
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalLocation,
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to /Tools when cancelling update from /Tools/Update path', async () => {
|
||||
const { WebguiUpdateCancel } = await import('~/composables/services/webgui');
|
||||
const originalLocation = window.location;
|
||||
let hrefValue = '';
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: {
|
||||
...originalLocation,
|
||||
pathname: '/Tools/Update',
|
||||
get href() {
|
||||
return hrefValue;
|
||||
},
|
||||
set href(value) {
|
||||
hrefValue = value;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(WebguiUpdateCancel).mockResolvedValueOnce({ success: true });
|
||||
|
||||
await store.cancelUpdate();
|
||||
|
||||
expect(WebguiUpdateCancel).toHaveBeenCalled();
|
||||
expect(hrefValue).toBe('/Tools');
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: originalLocation,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error when cancel update is unsuccessful', async () => {
|
||||
const { WebguiUpdateCancel } = await import('~/composables/services/webgui');
|
||||
|
||||
vi.mocked(WebguiUpdateCancel).mockResolvedValueOnce({ success: false });
|
||||
|
||||
await expect(store.cancelUpdate()).rejects.toThrow('Unable to cancel update');
|
||||
|
||||
expect(WebguiUpdateCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error when WebguiUpdateCancel fails', async () => {
|
||||
const { WebguiUpdateCancel } = await import('~/composables/services/webgui');
|
||||
|
||||
vi.mocked(WebguiUpdateCancel).mockRejectedValueOnce(new Error('API error'));
|
||||
|
||||
await expect(store.cancelUpdate()).rejects.toThrow(
|
||||
'[cancelUpdate] Error cancelling update with error: API error'
|
||||
);
|
||||
|
||||
expect(WebguiUpdateCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
422
web/__test__/store/updateOsActions.test.ts
Normal file
422
web/__test__/store/updateOsActions.test.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* UpdateOsActions store test coverage
|
||||
*/
|
||||
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalUpdateOsAction } from '@unraid/shared-callbacks';
|
||||
import type { Release } from '~/store/updateOsActions';
|
||||
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
|
||||
vi.mock('~/helpers/urls', () => ({
|
||||
WEBGUI_TOOLS_UPDATE: {
|
||||
toString: () => 'https://webgui/tools/update',
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUpdateOs = vi.fn();
|
||||
const mockInstall = vi.fn();
|
||||
const mockGetOsReleaseBySha256 = vi.fn();
|
||||
const mockAlert = vi.fn();
|
||||
const mockDocumentSubmit = vi.fn();
|
||||
|
||||
vi.stubGlobal('alert', mockAlert);
|
||||
|
||||
const mockDocument = {};
|
||||
Object.defineProperty(mockDocument, 'rebootNow', {
|
||||
value: { submit: mockDocumentSubmit },
|
||||
writable: true,
|
||||
});
|
||||
vi.stubGlobal('document', mockDocument);
|
||||
vi.stubGlobal('openChanges', vi.fn());
|
||||
vi.stubGlobal('openBox', vi.fn());
|
||||
|
||||
vi.mock('~/composables/installPlugin', () => ({
|
||||
default: () => ({
|
||||
install: mockInstall,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/composables/services/releases', () => ({
|
||||
getOsReleaseBySha256: (payload: unknown) => mockGetOsReleaseBySha256(payload),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/account', () => ({
|
||||
useAccountStore: () => ({
|
||||
updateOs: mockUpdateOs,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/server', () => ({
|
||||
useServerStore: () => ({
|
||||
guid: 'test-guid',
|
||||
keyfile: 'test-keyfile',
|
||||
osVersion: '6.12.4',
|
||||
osVersionBranch: 'stable',
|
||||
regUpdatesExpired: false,
|
||||
rebootType: '',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('~/store/updateOs', () => ({
|
||||
useUpdateOsStore: () => ({
|
||||
available: '6.12.5',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('UpdateOsActions Store', () => {
|
||||
let store: ReturnType<typeof useUpdateOsActionsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useUpdateOsActionsStore();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Suppress console output during tests
|
||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('Basic functionality', () => {
|
||||
it('should initialize with default values', () => {
|
||||
expect(store.status).toBe('ready');
|
||||
expect(store.callbackTypeDowngrade).toBe(false);
|
||||
expect(store.callbackUpdateRelease).toBeNull();
|
||||
});
|
||||
|
||||
it('should set update OS action', () => {
|
||||
const action: ExternalUpdateOsAction = {
|
||||
type: 'updateOs',
|
||||
sha256: 'test-sha256',
|
||||
};
|
||||
|
||||
store.setUpdateOsAction(action);
|
||||
|
||||
mockGetOsReleaseBySha256.mockResolvedValue({
|
||||
version: '6.12.5',
|
||||
name: 'Test Release',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
});
|
||||
|
||||
return store.actOnUpdateOsAction().then(() => {
|
||||
expect(mockGetOsReleaseBySha256).toHaveBeenCalledWith({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should confirm update OS', () => {
|
||||
const release: Release = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
basefile: 'unRAIDServer-6.12.5-x86_64.zip',
|
||||
date: '2023-10-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
store.confirmUpdateOs(release);
|
||||
|
||||
expect(store.callbackUpdateRelease).toEqual(release);
|
||||
expect(store.status).toBe('confirming');
|
||||
});
|
||||
|
||||
it('should get release from key server', async () => {
|
||||
const mockRelease: Release = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
basefile: 'unRAIDServer-6.12.5-x86_64.zip',
|
||||
date: '2023-10-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
mockGetOsReleaseBySha256.mockResolvedValue(mockRelease);
|
||||
|
||||
const result = await store.getReleaseFromKeyServer({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
|
||||
expect(mockGetOsReleaseBySha256).toHaveBeenCalledWith({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
expect(result).toEqual(mockRelease);
|
||||
});
|
||||
|
||||
it('should throw error when getting release without keyfile', async () => {
|
||||
await expect(
|
||||
store.getReleaseFromKeyServer({
|
||||
keyfile: '',
|
||||
sha256: 'test-sha256',
|
||||
})
|
||||
).rejects.toThrow('No payload.keyfile provided');
|
||||
});
|
||||
|
||||
it('should throw error when getting release without sha256', async () => {
|
||||
await expect(
|
||||
store.getReleaseFromKeyServer({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: '',
|
||||
})
|
||||
).rejects.toThrow('No payload.sha256 provided');
|
||||
});
|
||||
|
||||
it('should throw error when getOsReleaseBySha256 fails', async () => {
|
||||
mockGetOsReleaseBySha256.mockRejectedValue(new Error('API error'));
|
||||
|
||||
await expect(
|
||||
store.getReleaseFromKeyServer({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
})
|
||||
).rejects.toThrow('Unable to get release from keyserver');
|
||||
});
|
||||
|
||||
it('should call actOnUpdateOsAction correctly for upgrade', async () => {
|
||||
const mockRelease: Release = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
basefile: 'unRAIDServer-6.12.5-x86_64.zip',
|
||||
date: '2023-10-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
mockGetOsReleaseBySha256.mockResolvedValue(mockRelease);
|
||||
|
||||
// Set the update action first
|
||||
store.setUpdateOsAction({
|
||||
type: 'updateOs',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
|
||||
await store.actOnUpdateOsAction();
|
||||
|
||||
expect(mockGetOsReleaseBySha256).toHaveBeenCalledWith({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
expect(store.callbackTypeDowngrade).toBe(false);
|
||||
expect(store.callbackUpdateRelease).toEqual(mockRelease);
|
||||
expect(store.status).toBe('confirming');
|
||||
});
|
||||
|
||||
it('should call actOnUpdateOsAction correctly for downgrade', async () => {
|
||||
const mockRelease: Release = {
|
||||
version: '6.12.3',
|
||||
name: 'Unraid 6.12.3',
|
||||
basefile: 'unRAIDServer-6.12.3-x86_64.zip',
|
||||
date: '2023-05-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
mockGetOsReleaseBySha256.mockResolvedValue(mockRelease);
|
||||
|
||||
store.setUpdateOsAction({
|
||||
type: 'updateOs',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
|
||||
await store.actOnUpdateOsAction(true);
|
||||
|
||||
expect(mockGetOsReleaseBySha256).toHaveBeenCalledWith({
|
||||
keyfile: 'test-keyfile',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
expect(store.callbackTypeDowngrade).toBe(true);
|
||||
expect(store.callbackUpdateRelease).toEqual(mockRelease);
|
||||
expect(store.status).toBe('confirming');
|
||||
});
|
||||
|
||||
it('should throw error when release version matches current version', async () => {
|
||||
const mockRelease: Release = {
|
||||
version: '6.12.4',
|
||||
name: 'Unraid 6.12.4',
|
||||
basefile: 'unRAIDServer-6.12.4-x86_64.zip',
|
||||
date: '2023-05-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
mockGetOsReleaseBySha256.mockResolvedValue(mockRelease);
|
||||
|
||||
store.setUpdateOsAction({
|
||||
type: 'updateOs',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
|
||||
await expect(store.actOnUpdateOsAction()).rejects.toThrow(
|
||||
"Release version is the same as the server's current version"
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when release is not found', async () => {
|
||||
mockGetOsReleaseBySha256.mockResolvedValue(null);
|
||||
|
||||
store.setUpdateOsAction({
|
||||
type: 'updateOs',
|
||||
sha256: 'test-sha256',
|
||||
});
|
||||
|
||||
await expect(store.actOnUpdateOsAction()).rejects.toThrow('Release not found');
|
||||
});
|
||||
|
||||
it('should install OS update when update release is set', () => {
|
||||
const release: Release = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
basefile: 'unRAIDServer-6.12.5-x86_64.zip',
|
||||
date: '2023-10-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
store.confirmUpdateOs(release);
|
||||
store.installOsUpdate();
|
||||
|
||||
expect(store.status).toBe('updating');
|
||||
expect(mockInstall).toHaveBeenCalledWith({
|
||||
modalTitle: 'Unraid 6.12.5 Update',
|
||||
pluginUrl: 'https://example.com/plugin.plg',
|
||||
update: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should install OS downgrade when downgrade flag is set', () => {
|
||||
const release: Release = {
|
||||
version: '6.12.3',
|
||||
name: 'Unraid 6.12.3',
|
||||
basefile: 'unRAIDServer-6.12.3-x86_64.zip',
|
||||
date: '2023-05-15',
|
||||
url: 'https://example.com/download.zip',
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
md5: 'abc123',
|
||||
size: '400000000',
|
||||
sha256: 'test-sha256',
|
||||
plugin_url: 'https://example.com/plugin.plg',
|
||||
plugin_sha256: 'plugin-sha256',
|
||||
announce_url: 'https://example.com/announce',
|
||||
};
|
||||
|
||||
store.confirmUpdateOs(release);
|
||||
store.callbackTypeDowngrade = true;
|
||||
store.installOsUpdate();
|
||||
|
||||
expect(store.status).toBe('updating');
|
||||
expect(mockInstall).toHaveBeenCalledWith({
|
||||
modalTitle: 'Unraid 6.12.3 Downgrade',
|
||||
pluginUrl: 'https://example.com/plugin.plg',
|
||||
update: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log error when trying to install without release', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
store.installOsUpdate();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[installOsUpdate] release not found');
|
||||
expect(mockInstall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reboot server', () => {
|
||||
store.rebootServer();
|
||||
|
||||
expect(mockDocumentSubmit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should view release notes using openChanges when available', () => {
|
||||
const openChangesSpy = vi.fn();
|
||||
vi.stubGlobal('openChanges', openChangesSpy);
|
||||
|
||||
store.viewReleaseNotes('Test Release Notes');
|
||||
|
||||
expect(openChangesSpy).toHaveBeenCalledWith(
|
||||
'showchanges /var/tmp/unRAIDServer.txt',
|
||||
'Test Release Notes'
|
||||
);
|
||||
});
|
||||
|
||||
it('should view release notes using openBox when openChanges not available', () => {
|
||||
const openBoxSpy = vi.fn();
|
||||
vi.stubGlobal('openChanges', undefined);
|
||||
vi.stubGlobal('openBox', openBoxSpy);
|
||||
|
||||
store.viewReleaseNotes('Test Release Notes');
|
||||
|
||||
expect(openBoxSpy).toHaveBeenCalledWith(
|
||||
'/plugins/dynamix.plugin.manager/include/ShowChanges.php?file=/var/tmp/unRAIDServer.txt',
|
||||
'Test Release Notes',
|
||||
600,
|
||||
900
|
||||
);
|
||||
});
|
||||
|
||||
it('should show alert when neither openChanges nor openBox are available', () => {
|
||||
vi.stubGlobal('openChanges', undefined);
|
||||
vi.stubGlobal('openBox', undefined);
|
||||
|
||||
store.viewReleaseNotes('Test Release Notes');
|
||||
|
||||
expect(mockAlert).toHaveBeenCalledWith('Unable to open release notes');
|
||||
});
|
||||
|
||||
it('should set status', () => {
|
||||
store.setStatus('checking');
|
||||
expect(store.status).toBe('checking');
|
||||
|
||||
store.setStatus('updating');
|
||||
expect(store.status).toBe('updating');
|
||||
});
|
||||
});
|
||||
});
|
||||
197
web/__test__/store/updateOsChangelog.test.ts
Normal file
197
web/__test__/store/updateOsChangelog.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* UpdateOsChangelog store test coverage
|
||||
*/
|
||||
|
||||
import { nextTick } from 'vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ServerUpdateOsResponse } from '~/types/server';
|
||||
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
vi.mock('~/helpers/markdown', () => ({
|
||||
Markdown: {
|
||||
create: () => ({
|
||||
setOptions: vi.fn(),
|
||||
parse: vi.fn().mockResolvedValue('<h1>Test Title</h1><p>Test content</p>'),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('~/helpers/urls', () => ({
|
||||
DOCS_RELEASE_NOTES: {
|
||||
toString: () => 'https://docs.unraid.net/unraid-os/release-notes/',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('marked-base-url', () => ({
|
||||
baseUrl: vi.fn().mockReturnValue(vi.fn()),
|
||||
}));
|
||||
|
||||
vi.mock('semver/functions/prerelease', () => ({
|
||||
default: vi.fn((version) => (version && version.includes('-') ? ['beta', '1'] : null)),
|
||||
}));
|
||||
|
||||
const mockRequestText = vi.fn().mockResolvedValue('# Test Changelog\n\nTest content');
|
||||
vi.mock('~/composables/services/request', () => ({
|
||||
request: {
|
||||
url: () => ({
|
||||
get: () => ({
|
||||
text: mockRequestText,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockSend = vi.fn();
|
||||
vi.mock('~/store/callbackActions', () => ({
|
||||
useCallbackActionsStore: () => ({
|
||||
send: mockSend,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockStableRelease: Partial<ServerUpdateOsResponse> = {
|
||||
version: '6.12.5',
|
||||
name: 'Unraid 6.12.5',
|
||||
date: '2023-10-15',
|
||||
isEligible: true,
|
||||
isNewer: true,
|
||||
changelog: 'https://example.com/changelog.md',
|
||||
changelogPretty: 'https://example.com/changelog',
|
||||
sha256: 'test-sha256',
|
||||
};
|
||||
|
||||
const mockBetaRelease: Partial<ServerUpdateOsResponse> = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.5-beta1',
|
||||
};
|
||||
|
||||
describe('UpdateOsChangelog Store', () => {
|
||||
let store: ReturnType<typeof useUpdateOsChangelogStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
store = useUpdateOsChangelogStore();
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Suppress console output
|
||||
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('Store API', () => {
|
||||
it('should initialize with default values', () => {
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
expect(store.parseChangelogFailed).toBe('');
|
||||
});
|
||||
|
||||
it('should set and get releaseForUpdate', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
expect(store.releaseForUpdate).toEqual(mockStableRelease);
|
||||
|
||||
store.setReleaseForUpdate(null);
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
});
|
||||
|
||||
it('should determine if release is stable', () => {
|
||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
||||
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(store.isReleaseForUpdateStable).toBe(true);
|
||||
|
||||
store.setReleaseForUpdate(mockBetaRelease as ServerUpdateOsResponse);
|
||||
expect(store.isReleaseForUpdateStable).toBe(false);
|
||||
});
|
||||
|
||||
it('should have a method to fetch and confirm install', () => {
|
||||
store.fetchAndConfirmInstall('test-sha256');
|
||||
|
||||
expect(mockSend).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
[
|
||||
{
|
||||
sha256: 'test-sha256',
|
||||
type: 'updateOs',
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
'forUpc'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have computed properties for changelog display', async () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(typeof store.mutatedParsedChangelog).toBe('string');
|
||||
expect(typeof store.parsedChangelogTitle).toBe('string');
|
||||
});
|
||||
|
||||
it('should clear changelog data when release is set to null', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
store.setReleaseForUpdate(null);
|
||||
|
||||
expect(store.releaseForUpdate).toBeNull();
|
||||
expect(store.parseChangelogFailed).toBe('');
|
||||
});
|
||||
|
||||
it('should handle state transitions when changing releases', () => {
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
const differentRelease = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.6',
|
||||
};
|
||||
store.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
||||
|
||||
expect(store.releaseForUpdate).toEqual(differentRelease);
|
||||
});
|
||||
|
||||
it('should have proper error handling for failed requests', async () => {
|
||||
mockRequestText.mockRejectedValueOnce(new Error('Network error'));
|
||||
store.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(store.parseChangelogFailed).toBeTruthy();
|
||||
expect(store.parseChangelogFailed).toContain('error');
|
||||
});
|
||||
|
||||
it('should fetch and parse changelog when releaseForUpdate changes', async () => {
|
||||
const internalStore = useUpdateOsChangelogStore();
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
internalStore.setReleaseForUpdate(mockStableRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).toHaveBeenCalled();
|
||||
|
||||
mockRequestText.mockClear();
|
||||
|
||||
const differentRelease = {
|
||||
...mockStableRelease,
|
||||
version: '6.12.6',
|
||||
changelog: 'https://example.com/different-changelog.md',
|
||||
};
|
||||
|
||||
internalStore.setReleaseForUpdate(differentRelease as ServerUpdateOsResponse);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).toHaveBeenCalled();
|
||||
|
||||
mockRequestText.mockClear();
|
||||
|
||||
internalStore.setReleaseForUpdate(null);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(mockRequestText).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -179,5 +179,6 @@ export const useCallbackActionsStore = defineStore('callbackActions', () => {
|
||||
// helpers
|
||||
sendType: 'fromUpc',
|
||||
encryptionKey: import.meta.env.VITE_CALLBACK_KEY,
|
||||
callbackError,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
@@ -10,8 +11,12 @@ setActivePinia(createPinia());
|
||||
export const useModalStore = defineStore('modal', () => {
|
||||
const modalVisible = ref<boolean>(true);
|
||||
|
||||
const modalHide = () => { modalVisible.value = false; };
|
||||
const modalShow = () => { modalVisible.value = true; };
|
||||
const modalHide = () => {
|
||||
modalVisible.value = false;
|
||||
};
|
||||
const modalShow = () => {
|
||||
modalVisible.value = true;
|
||||
};
|
||||
const modalToggle = useToggle(modalVisible);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import type { NotificationFragmentFragment } from '~/composables/gql/graphql';
|
||||
|
||||
setActivePinia(createPinia());
|
||||
@@ -7,9 +9,11 @@ export const useNotificationsStore = defineStore('notifications', () => {
|
||||
const notifications = ref<NotificationFragmentFragment[]>([]);
|
||||
const isOpen = ref<boolean>(false);
|
||||
|
||||
const title = computed<string>(() => isOpen.value ? 'Notifications Are Open' : 'Notifications Are Closed');
|
||||
const title = computed<string>(() =>
|
||||
isOpen.value ? 'Notifications Are Open' : 'Notifications Are Closed'
|
||||
);
|
||||
|
||||
const toggle = () => isOpen.value = !isOpen.value;
|
||||
const toggle = () => (isOpen.value = !isOpen.value);
|
||||
|
||||
const setNotifications = (newNotifications: NotificationFragmentFragment[]) => {
|
||||
notifications.value = newNotifications;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { PURCHASE_CALLBACK } from '~/helpers/urls';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
@@ -21,66 +23,76 @@ export const usePurchaseStore = defineStore('purchase', () => {
|
||||
const activate = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
},
|
||||
type: 'activate',
|
||||
},
|
||||
type: 'activate',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const redeem = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
},
|
||||
type: 'redeem',
|
||||
},
|
||||
type: 'redeem',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const purchase = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
},
|
||||
type: 'purchase',
|
||||
},
|
||||
type: 'purchase',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const upgrade = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
},
|
||||
type: 'upgrade',
|
||||
},
|
||||
type: 'upgrade',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
const renew = () => {
|
||||
callbackStore.send(
|
||||
PURCHASE_CALLBACK.toString(),
|
||||
[{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
[
|
||||
{
|
||||
server: {
|
||||
...serverPurchasePayload.value,
|
||||
},
|
||||
type: 'renew',
|
||||
},
|
||||
type: 'renew',
|
||||
}],
|
||||
],
|
||||
inIframe.value ? 'newTab' : undefined,
|
||||
sendType.value,
|
||||
sendType.value
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* New key replacement, should happen also on server side.
|
||||
* Cron to run hourly, check on how many days are left until regExp…within X days then allow request to be done
|
||||
*/
|
||||
import { h } from 'vue';
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import {
|
||||
@@ -263,5 +263,6 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
|
||||
purgeValidationResponse,
|
||||
setReplaceStatus,
|
||||
setRenewStatus,
|
||||
error,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { defaultColors } from '~/themes/default';
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import type { ExternalPayload, TrialExtend, TrialStart } from '@unraid/shared-callbacks';
|
||||
import type { StartTrialResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
|
||||
import { startTrial, type StartTrialResponse } from '~/composables/services/keyServer';
|
||||
|
||||
import { startTrial } from '~/composables/services/keyServer';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { ExternalPayload, TrialExtend, TrialStart } from '@unraid/shared-callbacks';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
*/
|
||||
*/
|
||||
setActivePinia(createPinia());
|
||||
|
||||
export const useTrialStore = defineStore('trial', () => {
|
||||
@@ -22,8 +24,15 @@ export const useTrialStore = defineStore('trial', () => {
|
||||
type TrialStatus = 'failed' | 'ready' | TrialExtend | TrialStart | 'success';
|
||||
const trialStatus = ref<TrialStatus>('ready');
|
||||
|
||||
const trialModalLoading = computed(() => trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
|
||||
const trialModalVisible = computed(() => trialStatus.value === 'failed' || trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
|
||||
const trialModalLoading = computed(
|
||||
() => trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart'
|
||||
);
|
||||
const trialModalVisible = computed(
|
||||
() =>
|
||||
trialStatus.value === 'failed' ||
|
||||
trialStatus.value === 'trialExtend' ||
|
||||
trialStatus.value === 'trialStart'
|
||||
);
|
||||
|
||||
const requestTrial = async (type?: TrialExtend | TrialStart) => {
|
||||
try {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { ApolloClient as ApolloClientType, NormalizedCacheObject } from '@apollo/client';
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid';
|
||||
import { WebguiUnraidApiCommand } from '~/composables/services/webgui';
|
||||
import { client } from '~/helpers/create-apollo-client';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
// import { logErrorMessages } from '@vue/apollo-util';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/solid';
|
||||
import { client } from '~/helpers/create-apollo-client';
|
||||
|
||||
import type { ApolloClient as ApolloClientType, NormalizedCacheObject } from '@apollo/client';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
import { WebguiUnraidApiCommand } from '~/composables/services/webgui';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
import { useLazyQuery, useMutation } from "@vue/apollo-composable";
|
||||
import type { SetupRemoteAccessInput } from "~/composables/gql/graphql";
|
||||
import { defineStore } from 'pinia';
|
||||
import { useLazyQuery, useMutation } from '@vue/apollo-composable';
|
||||
|
||||
import type { SetupRemoteAccessInput } from '~/composables/gql/graphql';
|
||||
|
||||
import {
|
||||
GET_ALLOWED_ORIGINS,
|
||||
GET_REMOTE_ACCESS,
|
||||
SETUP_REMOTE_ACCESS,
|
||||
SET_ADDITIONAL_ALLOWED_ORIGINS,
|
||||
} from "~/store/unraidApiSettings.fragment";
|
||||
SETUP_REMOTE_ACCESS,
|
||||
} from '~/store/unraidApiSettings.fragment';
|
||||
|
||||
export const useUnraidApiSettingsStore = defineStore(
|
||||
"unraidApiSettings",
|
||||
() => {
|
||||
const { load: loadOrigins, result: origins } =
|
||||
useLazyQuery(GET_ALLOWED_ORIGINS);
|
||||
export const useUnraidApiSettingsStore = defineStore('unraidApiSettings', () => {
|
||||
const { load: loadOrigins, result: origins } = useLazyQuery(GET_ALLOWED_ORIGINS);
|
||||
|
||||
const { mutate: mutateOrigins } = useMutation(
|
||||
SET_ADDITIONAL_ALLOWED_ORIGINS
|
||||
);
|
||||
const { load: loadRemoteAccess, result: remoteAccessResult } =
|
||||
useLazyQuery(GET_REMOTE_ACCESS);
|
||||
const { mutate: mutateOrigins } = useMutation(SET_ADDITIONAL_ALLOWED_ORIGINS);
|
||||
const { load: loadRemoteAccess, result: remoteAccessResult } = useLazyQuery(GET_REMOTE_ACCESS);
|
||||
|
||||
const { mutate: setupRemoteAccessMutation } =
|
||||
useMutation(SETUP_REMOTE_ACCESS);
|
||||
const getAllowedOrigins = async () => {
|
||||
await loadOrigins();
|
||||
return origins?.value?.extraAllowedOrigins ?? [];
|
||||
};
|
||||
const { mutate: setupRemoteAccessMutation } = useMutation(SETUP_REMOTE_ACCESS);
|
||||
const getAllowedOrigins = async () => {
|
||||
await loadOrigins();
|
||||
return origins?.value?.extraAllowedOrigins ?? [];
|
||||
};
|
||||
|
||||
const setAllowedOrigins = async (origins: string[]) => {
|
||||
const result = await mutateOrigins({ input: { origins } });
|
||||
return result?.data?.setAdditionalAllowedOrigins;
|
||||
};
|
||||
const setAllowedOrigins = async (origins: string[]) => {
|
||||
const result = await mutateOrigins({ input: { origins } });
|
||||
return result?.data?.setAdditionalAllowedOrigins;
|
||||
};
|
||||
|
||||
const getRemoteAccess = async () => {
|
||||
await loadRemoteAccess();
|
||||
return remoteAccessResult?.value?.remoteAccess;
|
||||
};
|
||||
const getRemoteAccess = async () => {
|
||||
await loadRemoteAccess();
|
||||
return remoteAccessResult?.value?.remoteAccess;
|
||||
};
|
||||
|
||||
const setupRemoteAccess = async (input: SetupRemoteAccessInput) => {
|
||||
const response = await setupRemoteAccessMutation({ input });
|
||||
return response?.data?.setupRemoteAccess;
|
||||
};
|
||||
const setupRemoteAccess = async (input: SetupRemoteAccessInput) => {
|
||||
const response = await setupRemoteAccessMutation({ input });
|
||||
return response?.data?.setupRemoteAccess;
|
||||
};
|
||||
|
||||
return {
|
||||
getAllowedOrigins,
|
||||
setAllowedOrigins,
|
||||
getRemoteAccess,
|
||||
setupRemoteAccess,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
getAllowedOrigins,
|
||||
setAllowedOrigins,
|
||||
getRemoteAccess,
|
||||
setupRemoteAccess,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import { createPinia, defineStore, setActivePinia } from 'pinia';
|
||||
|
||||
import { ArrowPathIcon, BellAlertIcon } from '@heroicons/vue/24/solid';
|
||||
import { defineStore, createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
import useInstallPlugin from '~/composables/installPlugin';
|
||||
import { getOsReleaseBySha256, type GetOsReleaseBySha256Payload } from '~/composables/services/releases';
|
||||
|
||||
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
|
||||
import type { ExternalUpdateOsAction } from '@unraid/shared-callbacks';
|
||||
import type { GetOsReleaseBySha256Payload } from '~/composables/services/releases';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
import useInstallPlugin from '~/composables/installPlugin';
|
||||
import { getOsReleaseBySha256 } from '~/composables/services/releases';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
// import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
import type { ExternalUpdateOsAction } from '@unraid/shared-callbacks';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
|
||||
* @see https://github.com/vuejs/pinia/discussions/1085
|
||||
@@ -56,7 +57,16 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
|
||||
const updateOsAvailable = computed(() => updateOsStore.available);
|
||||
/** used when coming back from callback, this will be the release to install */
|
||||
const status = ref<'confirming' | 'checking' | 'ineligible' | 'failed' | 'ready' | 'success' | 'updating' | 'downgrading'>('ready');
|
||||
const status = ref<
|
||||
| 'confirming'
|
||||
| 'checking'
|
||||
| 'ineligible'
|
||||
| 'failed'
|
||||
| 'ready'
|
||||
| 'success'
|
||||
| 'updating'
|
||||
| 'downgrading'
|
||||
>('ready');
|
||||
const callbackTypeDowngrade = ref<boolean>(false);
|
||||
const callbackUpdateRelease = ref<Release | null>(null);
|
||||
const rebootType = computed(() => serverStore.rebootType);
|
||||
@@ -74,8 +84,11 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const ineligible = computed(() => !guid.value || !keyfile.value || !osVersion.value || regUpdatesExpired.value);
|
||||
const ineligibleText = computed(() => { // translated in components
|
||||
const ineligible = computed(
|
||||
() => !guid.value || !keyfile.value || !osVersion.value || regUpdatesExpired.value
|
||||
);
|
||||
const ineligibleText = computed(() => {
|
||||
// translated in components
|
||||
if (!guid.value) {
|
||||
return 'A valid GUID is required to check for OS updates.';
|
||||
}
|
||||
@@ -86,8 +99,10 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
return 'A valid OS version is required to check for OS updates.';
|
||||
}
|
||||
if (regUpdatesExpired.value) {
|
||||
const base = 'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.';
|
||||
const addtlText = 'You are still eligible to access OS updates that were published on or before {1}.';
|
||||
const base =
|
||||
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.';
|
||||
const addtlText =
|
||||
'You are still eligible to access OS updates that were published on or before {1}.';
|
||||
return updateOsAvailable.value ? `${base} ${addtlText}` : base;
|
||||
}
|
||||
return '';
|
||||
@@ -121,7 +136,8 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const setUpdateOsAction = (payload: ExternalUpdateOsAction | undefined) => (updateAction.value = payload);
|
||||
const setUpdateOsAction = (payload: ExternalUpdateOsAction | undefined) =>
|
||||
(updateAction.value = payload);
|
||||
/**
|
||||
* @description When receiving the callback the Account update page we'll use the provided sha256 of the release to get the release from the keyserver
|
||||
*/
|
||||
@@ -164,7 +180,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
throw new Error('Release not found');
|
||||
}
|
||||
if (foundRelease.version === osVersion.value) {
|
||||
throw new Error('Release version is the same as the server\'s current version');
|
||||
throw new Error("Release version is the same as the server's current version");
|
||||
}
|
||||
confirmUpdateOs(foundRelease);
|
||||
};
|
||||
@@ -176,7 +192,9 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
|
||||
setStatus('updating');
|
||||
installPlugin({
|
||||
modalTitle: callbackTypeDowngrade.value ? `${callbackUpdateRelease.value.name} Downgrade` : `${callbackUpdateRelease.value.name} Update`,
|
||||
modalTitle: callbackTypeDowngrade.value
|
||||
? `${callbackUpdateRelease.value.name} Downgrade`
|
||||
: `${callbackUpdateRelease.value.name} Update`,
|
||||
pluginUrl: callbackUpdateRelease.value.plugin_url,
|
||||
update: false,
|
||||
});
|
||||
@@ -189,7 +207,7 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
/**
|
||||
* By default this will display current version's release notes
|
||||
*/
|
||||
const viewReleaseNotes = (modalTitle:string, webguiFilePath?:string|undefined) => {
|
||||
const viewReleaseNotes = (modalTitle: string, webguiFilePath?: string | undefined) => {
|
||||
// @ts-expect-error • global set in the webgui
|
||||
if (typeof openChanges === 'function') {
|
||||
// @ts-expect-error • global set in the webgui
|
||||
@@ -197,7 +215,12 @@ export const useUpdateOsActionsStore = defineStore('updateOsActions', () => {
|
||||
// @ts-expect-error • global set in the webgui
|
||||
} else if (typeof openBox === 'function') {
|
||||
// @ts-expect-error • global set in the webgui
|
||||
openBox(`/plugins/dynamix.plugin.manager/include/ShowChanges.php?file=${webguiFilePath ?? '/var/tmp/unRAIDServer.txt'}`, modalTitle, 600, 900);
|
||||
openBox(
|
||||
`/plugins/dynamix.plugin.manager/include/ShowChanges.php?file=${webguiFilePath ?? '/var/tmp/unRAIDServer.txt'}`,
|
||||
modalTitle,
|
||||
600,
|
||||
900
|
||||
);
|
||||
} else {
|
||||
alert('Unable to open release notes');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user