Files
api/web/__test__/store/server.test.ts
Eli Bosley 31c41027fc feat: translations now use crowdin (translate.unraid.net) (#1739)
<!-- 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 -->
2025-10-13 16:56:08 -04:00

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