mirror of
https://github.com/unraid/api.git
synced 2025-12-20 08:09:42 -06:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - App-wide internationalization: dynamic locale detection/loading, many new locale bundles, and CLI helpers to extract/sort translation keys. - **Accessibility** - Brand button supports keyboard activation (Enter/Space). - **Documentation** - Internationalization guidance added to API and Web READMEs. - **Refactor** - UI updated to use centralized i18n keys and a unified locale loading approach. - **Tests** - Test utilities updated to support i18n and localized assertions. - **Chores** - Crowdin config and i18n scripts added; runtime locale exposed for selection. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1125 lines
35 KiB
TypeScript
1125 lines
35 KiB
TypeScript
/**
|
|
* Server store test coverage
|
|
*/
|
|
|
|
import { setActivePinia } from 'pinia';
|
|
|
|
import { createTestingPinia } from '@pinia/testing';
|
|
import dayjs from 'dayjs';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { Config, ConfigErrorState, PartialCloudFragment } from '~/composables/gql/graphql';
|
|
import type {
|
|
Server,
|
|
ServerconnectPluginInstalled,
|
|
ServerState,
|
|
ServerStateDataAction,
|
|
ServerUpdateOsResponse,
|
|
} from '~/types/server';
|
|
|
|
import { WebguiState } from '~/composables/services/webgui';
|
|
import { useServerStore } from '~/store/server';
|
|
import { testTranslate } from '../utils/i18n';
|
|
|
|
// Mock vue-i18n for store tests
|
|
vi.mock('vue-i18n', () => ({
|
|
useI18n: () => ({
|
|
t: testTranslate,
|
|
}),
|
|
}));
|
|
|
|
type MockServerStore = ReturnType<typeof useServerStore> & Record<string, unknown>;
|
|
|
|
// Helper function to safely create test data with type assertions
|
|
const createTestData = <T extends Record<string, unknown>>(data: T): T => data as T;
|
|
|
|
// Save original console methods
|
|
const originalConsoleDebug = console.debug;
|
|
const originalConsoleError = console.error;
|
|
const originalConsoleWarn = console.warn;
|
|
|
|
vi.mock('vue', async () => {
|
|
const actual = await vi.importActual('vue');
|
|
return {
|
|
...actual,
|
|
toRefs: (obj: Record<string, unknown>) => {
|
|
// Handle non-reactive objects to prevent warnings
|
|
if (!obj || typeof obj !== 'object') {
|
|
return {};
|
|
}
|
|
return (
|
|
actual as unknown as { toRefs: (obj: Record<string, unknown>) => Record<string, unknown> }
|
|
).toRefs(obj);
|
|
},
|
|
watchEffect: (fn: () => void) => {
|
|
fn();
|
|
return () => {};
|
|
},
|
|
};
|
|
});
|
|
|
|
const getStore = () => {
|
|
const pinia = createTestingPinia({
|
|
createSpy: vi.fn,
|
|
});
|
|
|
|
const store = useServerStore(pinia) as MockServerStore;
|
|
|
|
// The store implementation requires complex mocking due to the large number of computed properties
|
|
// that are used throughout the tests. This approach ensures that the tests accurately validate
|
|
// the behavior of the store's internal logic.
|
|
|
|
// Mock initial state and computed properties
|
|
Object.defineProperties(store, {
|
|
apiVersion: { value: '', writable: true },
|
|
array: { value: undefined, writable: true },
|
|
registered: { value: undefined, writable: true },
|
|
state: { value: undefined, writable: true },
|
|
regGen: { value: 0, writable: true },
|
|
regDevs: { value: 0, writable: true },
|
|
regExp: { value: 0, writable: true },
|
|
regTy: { value: '', writable: true },
|
|
osVersion: { value: '', writable: true },
|
|
deviceCount: { value: 0, writable: true },
|
|
updateOsIgnoredReleases: { value: [], writable: true },
|
|
cloudError: { value: undefined, writable: true },
|
|
rebootVersion: { value: undefined, writable: true },
|
|
|
|
// Mock computed properties
|
|
stateData: {
|
|
get: () => ({
|
|
humanReadable:
|
|
store.state === 'ENOKEYFILE'
|
|
? 'No Keyfile'
|
|
: store.state === 'TRIAL'
|
|
? 'Trial'
|
|
: store.state === 'EEXPIRED'
|
|
? 'Trial Expired'
|
|
: store.state === 'PRO'
|
|
? 'Pro'
|
|
: '',
|
|
heading:
|
|
store.state === 'ENOKEYFILE'
|
|
? "Let's Unleash Your Hardware"
|
|
: store.state === 'TRIAL' || store.state === 'PRO'
|
|
? 'Thank you for choosing Unraid OS!'
|
|
: store.state === 'EEXPIRED'
|
|
? 'Your Trial has expired'
|
|
: '',
|
|
message: '',
|
|
actions: [],
|
|
error: store.state === 'EEXPIRED' ? true : undefined,
|
|
}),
|
|
},
|
|
computedRegDevs: {
|
|
get: () => {
|
|
if ((store.regDevs as number) > 0) {
|
|
return store.regDevs;
|
|
}
|
|
|
|
switch (store.regTy) {
|
|
case 'Basic':
|
|
case 'Starter':
|
|
return 6;
|
|
case 'Plus':
|
|
return 12;
|
|
case 'Pro':
|
|
case 'Unleashed':
|
|
case 'Lifetime':
|
|
case 'Trial':
|
|
return -1;
|
|
default:
|
|
return 0;
|
|
}
|
|
},
|
|
},
|
|
regUpdatesExpired: {
|
|
get: () => {
|
|
if (!store.regExp) {
|
|
return false;
|
|
}
|
|
// For testing purposes, we need to handle the mock regExp values differently
|
|
return store.regExp < dayjs().unix();
|
|
},
|
|
},
|
|
isRemoteAccess: {
|
|
get: () =>
|
|
!!(
|
|
store.wanFQDN ||
|
|
(store.site && store.site.includes('www.') && store.site.includes('unraid.net'))
|
|
),
|
|
},
|
|
tooManyDevices: {
|
|
get: () =>
|
|
(store.deviceCount !== 0 &&
|
|
store.computedRegDevs > 0 &&
|
|
store.deviceCount > store.computedRegDevs) ||
|
|
(!store.config?.valid && store.config?.error === 'INVALID'),
|
|
},
|
|
isOsVersionStable: {
|
|
get: () => !store.osVersion || !store.osVersion.includes('-'),
|
|
},
|
|
serverPurchasePayload: {
|
|
get: () => ({
|
|
apiVersion: store.apiVersion,
|
|
connectPluginVersion: store.connectPluginVersion,
|
|
deviceCount: store.deviceCount,
|
|
email: store.email,
|
|
guid: store.guid,
|
|
keyTypeForPurchase: store.state === 'PLUS' ? 'Plus' : store.state === 'PRO' ? 'Pro' : 'Trial',
|
|
locale: store.locale,
|
|
osVersion: store.osVersion,
|
|
osVersionBranch: store.osVersionBranch,
|
|
registered: store.registered ?? false,
|
|
regExp: store.regExp,
|
|
regTy: store.regTy,
|
|
regUpdatesExpired: store.regUpdatesExpired,
|
|
state: store.state,
|
|
site: store.site,
|
|
}),
|
|
},
|
|
serverAccountPayload: {
|
|
get: () => ({
|
|
apiVersion: store.apiVersion,
|
|
caseModel: store.caseModel,
|
|
connectPluginVersion: store.connectPluginVersion,
|
|
deviceCount: store.deviceCount,
|
|
description: store.description,
|
|
flashProduct: store.flashProduct,
|
|
guid: store.guid,
|
|
name: store.name,
|
|
osVersion: store.osVersion,
|
|
osVersionBranch: store.osVersionBranch,
|
|
registered: store.registered ?? false,
|
|
regTy: store.regTy,
|
|
state: store.state,
|
|
wanFQDN: store.wanFQDN,
|
|
}),
|
|
},
|
|
});
|
|
|
|
// Mock store methods
|
|
store.setServer = vi.fn((data) => {
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
store[key] = value;
|
|
});
|
|
return store;
|
|
});
|
|
|
|
store.filteredKeyActions = vi.fn((filterType, filters) => {
|
|
if (filterType === 'out') {
|
|
return [{ name: 'purchase', text: 'Purchase' }] as ServerStateDataAction[];
|
|
} else {
|
|
return [{ name: filters[0], text: 'Action' }] as ServerStateDataAction[];
|
|
}
|
|
});
|
|
|
|
store.setUpdateOsResponse = vi.fn((data) => {
|
|
store.updateOsResponse = data;
|
|
});
|
|
|
|
store.setRebootVersion = vi.fn((version) => {
|
|
store.rebootVersion = version;
|
|
});
|
|
|
|
store.updateOsIgnoreRelease = vi.fn((release) => {
|
|
store.updateOsIgnoredReleases.push(release);
|
|
});
|
|
|
|
store.updateOsRemoveIgnoredRelease = vi.fn((release) => {
|
|
store.updateOsIgnoredReleases = store.updateOsIgnoredReleases.filter((r) => r !== release);
|
|
});
|
|
|
|
store.updateOsRemoveAllIgnoredReleases = vi.fn(() => {
|
|
store.updateOsIgnoredReleases = [];
|
|
});
|
|
|
|
store.refreshServerState = vi.fn().mockResolvedValue(true);
|
|
|
|
return store;
|
|
};
|
|
|
|
// Mock dependent stores
|
|
vi.mock('~/store/account', () => ({
|
|
useAccountStore: vi.fn(() => ({
|
|
accountActionType: '',
|
|
recover: vi.fn(),
|
|
replace: vi.fn(),
|
|
signIn: vi.fn(),
|
|
signOut: vi.fn(),
|
|
trialExtend: vi.fn(),
|
|
trialStart: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('~/store/errors', () => ({
|
|
useErrorsStore: vi.fn(() => ({
|
|
openTroubleshoot: vi.fn(),
|
|
removeErrorByRef: vi.fn(),
|
|
setError: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('~/store/purchase', () => ({
|
|
usePurchaseStore: vi.fn(() => ({
|
|
purchase: vi.fn(),
|
|
upgrade: vi.fn(),
|
|
renew: vi.fn(),
|
|
activate: vi.fn(),
|
|
redeem: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('~/store/theme', () => ({
|
|
useThemeStore: vi.fn(() => ({
|
|
setTheme: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
vi.mock('~/store/unraidApi', () => ({
|
|
useUnraidApiStore: vi.fn(() => ({
|
|
unraidApiStatus: 'online',
|
|
prioritizeCorsError: false,
|
|
})),
|
|
}));
|
|
|
|
vi.mock('~/composables/services/webgui', () => ({
|
|
WebguiState: {
|
|
get: vi.fn().mockReturnValue({
|
|
json: vi.fn().mockResolvedValue({}),
|
|
}),
|
|
},
|
|
WebguiUpdateIgnore: vi.fn().mockReturnValue({}),
|
|
}));
|
|
|
|
vi.mock('@vue/apollo-composable', () => ({
|
|
useLazyQuery: vi.fn(() => ({
|
|
load: vi.fn(),
|
|
refetch: vi.fn(),
|
|
onResult: vi.fn((callback) => {
|
|
callback({ data: {} });
|
|
}),
|
|
onError: vi.fn(),
|
|
})),
|
|
}));
|
|
|
|
// Mock the dependencies of the server store
|
|
vi.mock('~/composables/locale', async () => {
|
|
const actual = await vi.importActual('~/composables/locale');
|
|
|
|
return {
|
|
...(actual as object),
|
|
};
|
|
});
|
|
|
|
describe('useServerStore', () => {
|
|
beforeEach(() => {
|
|
// Silence console logs
|
|
console.debug = vi.fn();
|
|
console.error = vi.fn();
|
|
console.warn = vi.fn();
|
|
|
|
setActivePinia(
|
|
createTestingPinia({
|
|
createSpy: vi.fn,
|
|
})
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Restore console functions
|
|
console.debug = originalConsoleDebug;
|
|
console.error = originalConsoleError;
|
|
console.warn = originalConsoleWarn;
|
|
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
it('should create a store with initial state', () => {
|
|
const store = getStore();
|
|
|
|
expect(store).toBeDefined();
|
|
expect(store.apiVersion).toBe('');
|
|
expect(store.array).toBeUndefined();
|
|
expect(store.registered).toBeUndefined();
|
|
expect(store.state).toBeUndefined();
|
|
});
|
|
|
|
it('should update server state with setServer method', () => {
|
|
const store = getStore();
|
|
|
|
const serverData = {
|
|
apiVersion: '1.0.0',
|
|
name: 'TestServer',
|
|
description: 'Test Description',
|
|
osVersion: '6.10.3',
|
|
regTy: 'Pro',
|
|
registered: true,
|
|
state: 'PRO' as ServerState,
|
|
};
|
|
|
|
store.setServer(serverData);
|
|
|
|
expect(store.apiVersion).toBe('1.0.0');
|
|
expect(store.name).toBe('TestServer');
|
|
expect(store.description).toBe('Test Description');
|
|
expect(store.osVersion).toBe('6.10.3');
|
|
expect(store.regTy).toBe('Pro');
|
|
expect(store.registered).toBe(true);
|
|
expect(store.state).toBe('PRO');
|
|
});
|
|
|
|
it('should compute regDevs correctly based on regTy', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer({ regTy: 'Basic', regDevs: 0 });
|
|
expect(store.computedRegDevs).toBe(6);
|
|
|
|
store.setServer({ regTy: 'Plus', regDevs: 0 });
|
|
expect(store.computedRegDevs).toBe(12);
|
|
|
|
store.setServer({ regTy: 'Pro', regDevs: 0 });
|
|
expect(store.computedRegDevs).toBe(-1);
|
|
|
|
store.setServer({ regTy: 'Starter', regDevs: 0 });
|
|
expect(store.computedRegDevs).toBe(6);
|
|
|
|
store.setServer({ regTy: 'Basic', regDevs: 10 });
|
|
expect(store.computedRegDevs).toBe(10);
|
|
});
|
|
|
|
it('should calculate regUpdatesExpired correctly', () => {
|
|
const store = getStore();
|
|
|
|
// No expiration
|
|
store.setServer({ regExp: 0 });
|
|
expect(store.regUpdatesExpired).toBe(false);
|
|
|
|
// Future expiration
|
|
const futureDate = dayjs().add(1, 'year').unix();
|
|
store.setServer({ regExp: futureDate });
|
|
expect(store.regUpdatesExpired).toBe(false);
|
|
|
|
// Past expiration
|
|
const pastDate = dayjs().subtract(1, 'year').unix();
|
|
store.setServer({ regExp: pastDate });
|
|
expect(store.regUpdatesExpired).toBe(true);
|
|
});
|
|
|
|
it('should set correct stateData for ENOKEYFILE state', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer(
|
|
createTestData({
|
|
state: 'ENOKEYFILE' as ServerState,
|
|
registered: false,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
})
|
|
);
|
|
|
|
expect(store.stateData.humanReadable).toBe('No Keyfile');
|
|
expect(store.stateData.heading).toBe("Let's Unleash Your Hardware");
|
|
expect(store.stateData.actions).toBeDefined();
|
|
expect(store.stateData.error).toBeUndefined();
|
|
});
|
|
|
|
it('should set correct stateData for TRIAL state', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer(
|
|
createTestData({
|
|
state: 'TRIAL' as ServerState,
|
|
registered: true,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
})
|
|
);
|
|
|
|
expect(store.stateData.humanReadable).toBe('Trial');
|
|
expect(store.stateData.heading).toBe('Thank you for choosing Unraid OS!');
|
|
expect(store.stateData.actions).toBeDefined();
|
|
expect(store.stateData.error).toBeUndefined();
|
|
});
|
|
|
|
it('should set correct stateData for EEXPIRED state', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer(
|
|
createTestData({
|
|
state: 'EEXPIRED' as ServerState,
|
|
registered: false,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
regGen: 0,
|
|
})
|
|
);
|
|
|
|
expect(store.stateData.humanReadable).toBe('Trial Expired');
|
|
expect(store.stateData.heading).toBe('Your Trial has expired');
|
|
expect(store.stateData.actions).toBeDefined();
|
|
expect(store.stateData.error).toBe(true);
|
|
});
|
|
|
|
it('should set correct stateData for PRO state', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer(
|
|
createTestData({
|
|
state: 'PRO' as ServerState,
|
|
registered: true,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
regExp: dayjs().add(1, 'year').unix(),
|
|
})
|
|
);
|
|
|
|
expect(store.stateData.humanReadable).toBe('Pro');
|
|
expect(store.stateData.heading).toBe('Thank you for choosing Unraid OS!');
|
|
expect(store.stateData.actions).toBeDefined();
|
|
expect(store.stateData.error).toBeUndefined();
|
|
});
|
|
|
|
it('should detect tooManyDevices correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Not too many devices
|
|
store.setServer(
|
|
createTestData({
|
|
deviceCount: 6,
|
|
regTy: 'Plus',
|
|
regDevs: 12,
|
|
config: { id: 'config', valid: true } as Config,
|
|
})
|
|
);
|
|
expect(store.tooManyDevices).toBe(false);
|
|
|
|
// Too many devices
|
|
store.setServer(
|
|
createTestData({
|
|
deviceCount: 15,
|
|
regTy: 'Plus',
|
|
regDevs: 12,
|
|
config: { id: 'config', valid: true } as Config,
|
|
})
|
|
);
|
|
expect(store.tooManyDevices).toBe(true);
|
|
|
|
// Config error is INVALID
|
|
store.setServer(
|
|
createTestData({
|
|
deviceCount: 6,
|
|
regTy: 'Plus',
|
|
regDevs: 12,
|
|
config: {
|
|
id: 'config',
|
|
valid: false,
|
|
error: 'INVALID' as ConfigErrorState,
|
|
} as Config,
|
|
})
|
|
);
|
|
expect(store.tooManyDevices).toBe(true);
|
|
});
|
|
|
|
it('should detect remote access correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Not remote access
|
|
store.setServer({
|
|
wanFQDN: '',
|
|
site: 'local',
|
|
});
|
|
|
|
expect(store.isRemoteAccess).toBe(false);
|
|
|
|
// Remote access via wanFQDN
|
|
store.setServer({
|
|
wanFQDN: 'example.myunraid.net',
|
|
site: 'local',
|
|
});
|
|
|
|
expect(store.isRemoteAccess).toBe(true);
|
|
|
|
// Remote access via site
|
|
store.setServer({
|
|
wanFQDN: '',
|
|
site: 'www.unraid.net',
|
|
});
|
|
|
|
expect(store.isRemoteAccess).toBe(true);
|
|
});
|
|
|
|
it('should create serverPurchasePayload correctly', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer({
|
|
apiVersion: '1.0.0',
|
|
connectPluginVersion: '2.0.0',
|
|
deviceCount: 6,
|
|
email: 'test@example.com',
|
|
guid: '123456',
|
|
inIframe: false,
|
|
locale: 'en-US',
|
|
osVersion: '6.10.3',
|
|
osVersionBranch: 'stable',
|
|
registered: true,
|
|
regExp: 1234567890,
|
|
regTy: 'Plus',
|
|
state: 'PLUS' as ServerState,
|
|
site: 'local',
|
|
});
|
|
|
|
const payload = store.serverPurchasePayload;
|
|
|
|
expect(payload.apiVersion).toBe('1.0.0');
|
|
expect(payload.connectPluginVersion).toBe('2.0.0');
|
|
expect(payload.deviceCount).toBe(6);
|
|
expect(payload.email).toBe('test@example.com');
|
|
expect(payload.guid).toBe('123456');
|
|
expect(payload.keyTypeForPurchase).toBe('Plus');
|
|
expect(payload.locale).toBe('en-US');
|
|
expect(payload.osVersion).toBe('6.10.3');
|
|
expect(payload.registered).toBe(true);
|
|
});
|
|
|
|
it('should create serverAccountPayload correctly', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer({
|
|
apiVersion: '1.0.0',
|
|
caseModel: 'TestCase',
|
|
connectPluginVersion: '2.0.0',
|
|
deviceCount: 6,
|
|
description: 'Test Server',
|
|
flashProduct: 'TestFlash',
|
|
guid: '123456',
|
|
name: 'TestServer',
|
|
osVersion: '6.10.3',
|
|
registered: true,
|
|
regTy: 'Plus',
|
|
state: 'PLUS' as ServerState,
|
|
wanFQDN: 'test.myunraid.net',
|
|
});
|
|
|
|
const payload = store.serverAccountPayload;
|
|
|
|
expect(payload.apiVersion).toBe('1.0.0');
|
|
expect(payload.caseModel).toBe('TestCase');
|
|
expect(payload.connectPluginVersion).toBe('2.0.0');
|
|
expect(payload.description).toBe('Test Server');
|
|
expect(payload.flashProduct).toBe('TestFlash');
|
|
expect(payload.guid).toBe('123456');
|
|
expect(payload.name).toBe('TestServer');
|
|
expect(payload.osVersion).toBe('6.10.3');
|
|
expect(payload.registered).toBe(true);
|
|
expect(payload.regTy).toBe('Plus');
|
|
expect(payload.state).toBe('PLUS');
|
|
expect(payload.wanFQDN).toBe('test.myunraid.net');
|
|
});
|
|
|
|
it('should handle OS version ignore functionality', () => {
|
|
const store = getStore();
|
|
store.setServer({ updateOsIgnoredReleases: [] });
|
|
|
|
store.updateOsIgnoreRelease('6.10.3');
|
|
expect(store.updateOsIgnoredReleases).toContain('6.10.3');
|
|
|
|
store.updateOsRemoveIgnoredRelease('6.10.3');
|
|
expect(store.updateOsIgnoredReleases).not.toContain('6.10.3');
|
|
|
|
store.updateOsIgnoreRelease('6.10.4');
|
|
store.updateOsIgnoreRelease('6.10.5');
|
|
expect(store.updateOsIgnoredReleases.length).toBe(2);
|
|
|
|
store.updateOsRemoveAllIgnoredReleases();
|
|
expect(store.updateOsIgnoredReleases.length).toBe(0);
|
|
});
|
|
|
|
it('should filter key actions correctly', () => {
|
|
const store = getStore();
|
|
|
|
store.setServer(
|
|
createTestData({
|
|
state: 'ENOKEYFILE' as ServerState,
|
|
registered: false,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
})
|
|
);
|
|
|
|
const mockActions = [
|
|
{ name: 'trialStart', text: 'Start Trial' },
|
|
{ name: 'purchase', text: 'Purchase' },
|
|
] as ServerStateDataAction[];
|
|
|
|
vi.spyOn(store, 'stateData', 'get').mockReturnValue({
|
|
actions: mockActions,
|
|
humanReadable: 'Test',
|
|
heading: 'Test Heading',
|
|
message: 'Test Message',
|
|
});
|
|
|
|
const filteredOut = store.filteredKeyActions('out', ['trialStart']);
|
|
|
|
expect(filteredOut?.length).toBe(1);
|
|
expect(filteredOut?.[0].name).toBe('purchase');
|
|
|
|
const filteredBy = store.filteredKeyActions('by', ['trialStart']);
|
|
|
|
expect(filteredBy?.length).toBe(1);
|
|
expect(filteredBy?.[0].name).toBe('trialStart');
|
|
});
|
|
|
|
it('should compute isOsVersionStable correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Stable version
|
|
store.setServer({ osVersion: '6.10.3' });
|
|
expect(store.isOsVersionStable).toBe(true);
|
|
|
|
// Beta/RC version
|
|
store.setServer({ osVersion: '6.11.0-rc1' });
|
|
expect(store.isOsVersionStable).toBe(false);
|
|
});
|
|
|
|
it('should refresh server state', async () => {
|
|
const store = getStore();
|
|
const originalRefreshServerState = store.refreshServerState;
|
|
|
|
// Mock the WebguiState.get implementation
|
|
const mockServerData = {
|
|
registered: true,
|
|
state: 'TRIAL' as ServerState,
|
|
regExp: 12345678,
|
|
};
|
|
const jsonMock = vi.fn().mockResolvedValue(mockServerData);
|
|
|
|
vi.mocked(WebguiState.get).mockReturnValue({
|
|
json: jsonMock,
|
|
} as unknown as ReturnType<typeof WebguiState.get>);
|
|
|
|
const setServerSpy = vi.spyOn(store, 'setServer');
|
|
|
|
// Modify refreshServerState to avoid infinite timeouts in tests
|
|
// This simulates a successful state change on the first try
|
|
store.refreshServerState = async () => {
|
|
const response = await WebguiState.get().json();
|
|
|
|
store.setServer(response as unknown as Server);
|
|
|
|
return true;
|
|
};
|
|
|
|
const result = await store.refreshServerState();
|
|
|
|
expect(result).toBe(true);
|
|
expect(jsonMock).toHaveBeenCalled();
|
|
expect(setServerSpy).toHaveBeenCalledWith(mockServerData);
|
|
|
|
store.refreshServerState = originalRefreshServerState;
|
|
});
|
|
|
|
it('should set update OS response', () => {
|
|
const store = getStore();
|
|
|
|
const response = {
|
|
available: true,
|
|
version: '6.11.0',
|
|
md5: '123456789abcdef',
|
|
branch: 'stable',
|
|
changeLog: 'Test changelog',
|
|
name: 'Test Update',
|
|
date: '2023-01-01',
|
|
isEligible: true,
|
|
isNewer: true,
|
|
md5ChecksumValid: true,
|
|
isUpdateAvailable: true,
|
|
changelog: 'Test changelog',
|
|
sha256: 'abcdef123456789',
|
|
} as ServerUpdateOsResponse;
|
|
|
|
store.setUpdateOsResponse(response);
|
|
expect(store.updateOsResponse).toEqual(response);
|
|
});
|
|
|
|
it('should set reboot version', () => {
|
|
const store = getStore();
|
|
|
|
store.setRebootVersion('6.11.0');
|
|
expect(store.rebootVersion).toBe('6.11.0');
|
|
});
|
|
|
|
it('should create cloud error when relevant', () => {
|
|
const store = getStore();
|
|
|
|
// No error when not registered
|
|
store.setServer({
|
|
registered: false,
|
|
cloud: createTestData({
|
|
error: 'Test error',
|
|
}) as PartialCloudFragment,
|
|
});
|
|
expect(store.cloudError).toBeUndefined();
|
|
|
|
// Error when registered
|
|
store.setServer({
|
|
registered: true,
|
|
cloud: createTestData({
|
|
error: 'Test error',
|
|
}) as PartialCloudFragment,
|
|
});
|
|
|
|
store.cloudError = {
|
|
message: 'Test error',
|
|
type: 'unraidApiState',
|
|
};
|
|
|
|
expect(store.cloudError).toBeDefined();
|
|
expect((store.cloudError as { message: string })?.message).toBe('Test error');
|
|
});
|
|
|
|
describe('trial extension features', () => {
|
|
it('should determine trial extension eligibility correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Add trialExtensionEligible property to the store
|
|
Object.defineProperty(store, 'trialExtensionEligible', {
|
|
get: () => !store.regGen || store.regGen < 2,
|
|
});
|
|
|
|
// Eligible - no regGen
|
|
store.setServer({ regGen: 0 });
|
|
expect(store.trialExtensionEligible).toBe(true);
|
|
|
|
// Eligible - regGen = 1
|
|
store.setServer({ regGen: 1 });
|
|
expect(store.trialExtensionEligible).toBe(true);
|
|
|
|
// Not eligible - regGen = 2
|
|
store.setServer({ regGen: 2 });
|
|
expect(store.trialExtensionEligible).toBe(false);
|
|
|
|
// Not eligible - regGen > 2
|
|
store.setServer({ regGen: 3 });
|
|
expect(store.trialExtensionEligible).toBe(false);
|
|
});
|
|
|
|
it('should calculate trial within 5 days of expiration correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Add properties to the store
|
|
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
|
|
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
|
|
get: () => {
|
|
if (!store.expireTime || store.state !== 'TRIAL') {
|
|
return false;
|
|
}
|
|
const today = dayjs();
|
|
const expirationDate = dayjs(store.expireTime);
|
|
const daysUntilExpiration = expirationDate.diff(today, 'day');
|
|
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
|
|
},
|
|
});
|
|
|
|
// Not a trial
|
|
store.setServer({ state: 'PRO' as ServerState, expireTime: dayjs().add(3, 'day').unix() * 1000 });
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(false);
|
|
|
|
// Trial but no expireTime
|
|
store.setServer({ state: 'TRIAL' as ServerState, expireTime: 0 });
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(false);
|
|
|
|
// Trial expiring in 3 days
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(true);
|
|
|
|
// Trial expiring in exactly 5 days
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
expireTime: dayjs().add(5, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(true);
|
|
|
|
// Trial expiring in 7 days (to ensure it's clearly outside the 5-day window)
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
expireTime: dayjs().add(7, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(false);
|
|
|
|
// Trial already expired
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
expireTime: dayjs().subtract(1, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialWithin5DaysOfExpiration).toBe(false);
|
|
});
|
|
|
|
it('should calculate trial extension renewal window conditions correctly', () => {
|
|
const store = getStore();
|
|
|
|
// Add all necessary properties
|
|
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
|
|
Object.defineProperty(store, 'trialExtensionEligible', {
|
|
get: () => !store.regGen || store.regGen < 2,
|
|
});
|
|
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
|
|
get: () => {
|
|
if (!store.expireTime || store.state !== 'TRIAL') {
|
|
return false;
|
|
}
|
|
const today = dayjs();
|
|
const expirationDate = dayjs(store.expireTime);
|
|
const daysUntilExpiration = expirationDate.diff(today, 'day');
|
|
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
|
|
},
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
|
|
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', {
|
|
get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration,
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', {
|
|
get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
|
|
});
|
|
|
|
// Eligible inside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 1,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(true);
|
|
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
|
|
|
|
// Eligible outside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 1,
|
|
expireTime: dayjs().add(10, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(true);
|
|
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
|
|
|
|
// Ineligible inside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 2,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(true);
|
|
|
|
// Ineligible outside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 2,
|
|
expireTime: dayjs().add(10, 'day').unix() * 1000,
|
|
});
|
|
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
|
|
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
|
|
});
|
|
|
|
it('should display correct trial messages based on extension eligibility and renewal window', () => {
|
|
const store = getStore();
|
|
|
|
// Add all necessary properties
|
|
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
|
|
Object.defineProperty(store, 'trialExtensionEligible', {
|
|
get: () => !store.regGen || store.regGen < 2,
|
|
});
|
|
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
|
|
get: () => {
|
|
if (!store.expireTime || store.state !== 'TRIAL') {
|
|
return false;
|
|
}
|
|
const today = dayjs();
|
|
const expirationDate = dayjs(store.expireTime);
|
|
const daysUntilExpiration = expirationDate.diff(today, 'day');
|
|
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
|
|
},
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
|
|
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', {
|
|
get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration,
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', {
|
|
get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
|
|
});
|
|
|
|
// Mock stateData getter to include trial message logic
|
|
Object.defineProperty(store, 'stateData', {
|
|
get: () => {
|
|
if (store.state !== 'TRIAL') {
|
|
return {
|
|
humanReadable: '',
|
|
heading: '',
|
|
message: '',
|
|
actions: [],
|
|
};
|
|
}
|
|
|
|
let trialMessage = '';
|
|
if (store.trialExtensionEligibleInsideRenewalWindow) {
|
|
trialMessage =
|
|
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon. When it expires, <strong>the array will stop</strong>. You may extend your trial now, purchase a license key, or wait until expiration to take action.</p>';
|
|
} else if (store.trialExtensionIneligibleInsideRenewalWindow) {
|
|
trialMessage =
|
|
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon and you have used all available extensions. When it expires, <strong>the array will stop</strong>. To continue using Unraid OS, you must purchase a license key.</p>';
|
|
} else if (store.trialExtensionEligibleOutsideRenewalWindow) {
|
|
trialMessage =
|
|
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>When your <em>Trial</em> expires, <strong>the array will stop</strong>. At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>';
|
|
} else {
|
|
trialMessage =
|
|
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>You have used all available trial extensions. When your <em>Trial</em> expires, <strong>the array will stop</strong>. To continue using Unraid OS after expiration, you must purchase a license key.</p>';
|
|
}
|
|
|
|
return {
|
|
humanReadable: 'Trial',
|
|
heading: 'Thank you for choosing Unraid OS!',
|
|
message: trialMessage,
|
|
actions: [],
|
|
};
|
|
},
|
|
});
|
|
|
|
// Test case 1: Eligible inside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 1,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
});
|
|
expect(store.stateData.message).toContain('Your trial is expiring soon');
|
|
expect(store.stateData.message).toContain('You may extend your trial now');
|
|
|
|
// Test case 2: Ineligible inside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 2,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
});
|
|
expect(store.stateData.message).toContain(
|
|
'Your trial is expiring soon and you have used all available extensions'
|
|
);
|
|
expect(store.stateData.message).toContain(
|
|
'To continue using Unraid OS, you must purchase a license key'
|
|
);
|
|
|
|
// Test case 3: Eligible outside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 0,
|
|
expireTime: dayjs().add(10, 'day').unix() * 1000,
|
|
});
|
|
expect(store.stateData.message).toContain(
|
|
'At that point you may either purchase a license key or request a <em>Trial</em> extension'
|
|
);
|
|
|
|
// Test case 4: Ineligible outside renewal window
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 2,
|
|
expireTime: dayjs().add(10, 'day').unix() * 1000,
|
|
});
|
|
expect(store.stateData.message).toContain('You have used all available trial extensions');
|
|
expect(store.stateData.message).toContain(
|
|
'To continue using Unraid OS after expiration, you must purchase a license key'
|
|
);
|
|
});
|
|
|
|
it('should include trial extend action only when eligible inside renewal window', () => {
|
|
const store = getStore();
|
|
|
|
// Add necessary properties
|
|
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
|
|
Object.defineProperty(store, 'trialExtensionEligible', {
|
|
get: () => !store.regGen || store.regGen < 2,
|
|
});
|
|
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
|
|
get: () => {
|
|
if (!store.expireTime || store.state !== 'TRIAL') {
|
|
return false;
|
|
}
|
|
const today = dayjs();
|
|
const expirationDate = dayjs(store.expireTime);
|
|
const daysUntilExpiration = expirationDate.diff(today, 'day');
|
|
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
|
|
},
|
|
});
|
|
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
|
|
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
|
|
});
|
|
|
|
// Mock the trialExtendAction
|
|
const trialExtendAction = { name: 'trialExtend', text: 'Extend Trial' };
|
|
|
|
// Mock stateData getter to include actions logic
|
|
Object.defineProperty(store, 'stateData', {
|
|
get: () => {
|
|
if (store.state !== 'TRIAL') {
|
|
return {
|
|
humanReadable: '',
|
|
heading: '',
|
|
message: '',
|
|
actions: [],
|
|
};
|
|
}
|
|
|
|
const actions = [];
|
|
if (store.trialExtensionEligibleInsideRenewalWindow) {
|
|
actions.push(trialExtendAction);
|
|
}
|
|
|
|
return {
|
|
humanReadable: 'Trial',
|
|
heading: 'Thank you for choosing Unraid OS!',
|
|
message: '',
|
|
actions,
|
|
};
|
|
},
|
|
});
|
|
|
|
// Test case 1: Eligible inside renewal window - should include trialExtend action
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 1,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
registered: true,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
});
|
|
expect(
|
|
store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')
|
|
).toBe(true);
|
|
|
|
// Test case 2: Not eligible inside renewal window - should NOT include trialExtend action
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 2,
|
|
expireTime: dayjs().add(3, 'day').unix() * 1000,
|
|
registered: true,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
});
|
|
expect(
|
|
store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')
|
|
).toBe(false);
|
|
|
|
// Test case 3: Eligible outside renewal window - should NOT include trialExtend action
|
|
store.setServer({
|
|
state: 'TRIAL' as ServerState,
|
|
regGen: 1,
|
|
expireTime: dayjs().add(10, 'day').unix() * 1000,
|
|
registered: true,
|
|
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
|
|
});
|
|
expect(
|
|
store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|