Compare commits

...

6 Commits

Author SHA1 Message Date
Zack Spear
c0c6f160e7 Merge dbf2887f03 into 287b3d25c3 2025-07-10 17:19:52 -07:00
Zack Spear
dbf2887f03 test: add tests for sever store trial extension eligibility and messaging
Implemented new tests in `server.test.ts` to validate trial extension eligibility, expiration conditions, and corresponding user messages. This includes checks for eligibility based on `regGen` values and expiration timing, ensuring accurate feedback for users regarding their trial status.
2025-07-10 17:18:07 -07:00
Zack Spear
287b3d25c3 refactor: trial message logic to utilize new computed properties for better clarity and maintainability 2025-07-10 11:31:45 -07:00
Zack Spear
82eaca400f refactor: streamline trial message logic in server store
Consolidated trial message handling to reduce redundancy and improve readability. The logic now checks conditions for trial expiration and extension eligibility more efficiently, ensuring users receive accurate information based on their trial status.
2025-07-09 12:02:27 -07:00
Zack Spear
43cff73b84 chore: remove console logs from server store 2025-07-08 17:18:52 -07:00
Zack Spear
01e9db5d51 feat: trial extension allowed within 5 days of expiration 2025-07-08 16:33:07 -07:00
3 changed files with 348 additions and 2 deletions

View File

@@ -764,4 +764,323 @@ describe('useServerStore', () => {
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);
});
});
});

View File

@@ -23,6 +23,10 @@
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>To support more storage devices as your server grows, click Upgrade Key.</p>",
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>",
"<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>": "<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>",
"<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>": "<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>",
"<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>": "<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>",
"<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>": "<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>",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>",

View File

@@ -495,6 +495,7 @@ export const useServerStore = defineStore('server', () => {
});
let messageEGUID = '';
let trialMessage = '';
const stateData = computed((): ServerStateData => {
switch (state.value) {
case 'ENOKEYFILE':
@@ -510,16 +511,26 @@ export const useServerStore = defineStore('server', () => {
'<p>Choose an option below, then use our <a href="https://unraid.net/getting-started" target="_blank" rel="noreffer noopener">Getting Started Guide</a> to configure your array in less than 15 minutes.</p>',
};
case 'TRIAL':
if (trialExtensionEligibleInsideRenewalWindow.value) {
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 (trialExtensionIneligibleInsideRenewalWindow.value) {
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 (trialExtensionEligibleOutsideRenewalWindow.value) {
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 { // would be trialExtensionIneligibleOutsideRenewalWindow if it wasn't an else conditionally
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 {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(trialExtensionEligibleInsideRenewalWindow.value ? [trialExtendAction.value] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Trial',
heading: 'Thank you for choosing Unraid OS!',
message:
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>',
message: trialMessage,
};
case 'EEXPIRED':
return {
@@ -773,6 +784,18 @@ export const useServerStore = defineStore('server', () => {
return stateData.value.actions.filter((action) => !authActionsNames.includes(action.name));
});
const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2);
const trialWithin5DaysOfExpiration = computed(() => {
if (!expireTime.value || state.value !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(expireTime.value);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
});
const trialExtensionEligibleInsideRenewalWindow = computed(() => trialExtensionEligible.value && trialWithin5DaysOfExpiration.value);
const trialExtensionEligibleOutsideRenewalWindow = computed(() => trialExtensionEligible.value && !trialWithin5DaysOfExpiration.value);
const trialExtensionIneligibleInsideRenewalWindow = computed(() => !trialExtensionEligible.value && trialWithin5DaysOfExpiration.value);
const serverConfigError = computed((): Error | undefined => {
if (!config.value?.valid && config.value?.error) {