Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions web/__test__/components/Onboarding/OnboardingModal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,6 @@ describe('OnboardingModal.vue', () => {
enableBootTransfer: 'yes',
},
};

Object.defineProperty(window, 'location', {
writable: true,
configurable: true,
value: {
href: '',
pathname: '/Dashboard',
},
});
});

const mountComponent = () => {
Expand Down Expand Up @@ -302,24 +293,6 @@ describe('OnboardingModal.vue', () => {
expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
});

it('does not render on login route', () => {
Object.defineProperty(window, 'location', {
writable: true,
configurable: true,
value: {
href: '',
pathname: '/login',
},
});

onboardingModalStoreState.isAutoVisible.value = true;
onboardingStatusStore.canDisplayOnboardingModal.value = true;

const wrapper = mountComponent();

expect(wrapper.find('[data-testid="dialog"]').exists()).toBe(false);
});

it('shows activation step for ENOKEYFILE1', () => {
activationCodeDataStore.registrationState.value = 'ENOKEYFILE1';
onboardingDraftStore.currentStepIndex.value = 4;
Expand Down
76 changes: 21 additions & 55 deletions web/__test__/components/Registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ServerconnectPluginInstalled } from '~/types/server';
import type { Pinia } from 'pinia';

import Registration from '~/components/Registration.standalone.vue';
import { useAccountStore } from '~/store/account';
import { usePurchaseStore } from '~/store/purchase';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
Expand Down Expand Up @@ -151,6 +152,7 @@ const t = testTranslate;
describe('Registration.standalone.vue', () => {
let wrapper: VueWrapper<unknown>;
let pinia: Pinia;
let accountStore: ReturnType<typeof useAccountStore>;
let serverStore: ReturnType<typeof useServerStore>;
let replaceRenewStore: ReturnType<typeof useReplaceRenewStore>;
let purchaseStore: ReturnType<typeof usePurchaseStore>;
Expand Down Expand Up @@ -184,6 +186,7 @@ describe('Registration.standalone.vue', () => {
});
setActivePinia(pinia);

accountStore = useAccountStore();
serverStore = useServerStore();
replaceRenewStore = useReplaceRenewStore();
purchaseStore = usePurchaseStore();
Expand Down Expand Up @@ -306,104 +309,67 @@ describe('Registration.standalone.vue', () => {
expect(attachedStorageDevicesItem?.props('text')).toBe('8 out of unlimited devices');
});

it('shows TPM transfer guidance when TPM licensing is available', async () => {
it('shows Move License to TPM when TPM licensing is available', async () => {
serverStore.state = 'PRO';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
serverStore.mdState = 'STOPPED';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.keyfile = 'keyfile-present';

await wrapper.vm.$nextTick();

const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]');
const moveButton = wrapper.find('[data-testid="move-license-to-tpm"]');

expect(transferNotice.exists()).toBe(true);
expect(transferNotice.text()).toContain('TPM licensing is available on this server.');
expect(transferNotice.text()).toContain('Stop the array.');
expect(transferNotice.text()).toContain('Remove the USB flash boot device.');
expect(transferNotice.text()).toContain('Refresh this page.');
expect(transferNotice.text()).toContain('Press Replace Key.');
expect(transferNotice.text()).toContain('Start the array.');
expect(transferNotice.text()).not.toContain('Tools > Registration');
expect(moveButton.exists()).toBe(true);
});

it('only checks the stop-array step when the array is stopped', async () => {
it('shows Move License to TPM when flashGuid is missing but the active GUID is still a flash GUID', async () => {
serverStore.state = 'PRO';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
serverStore.mdState = 'STARTED';
serverStore.flashGuid = '';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.keyfile = 'keyfile-present';

await wrapper.vm.$nextTick();

const transferNotice = wrapper.find('[data-testid="tpm-transfer-available"]');

expect(transferNotice.exists()).toBe(true);
expect(transferNotice.text()).not.toContain('[x] Stop the array.');
expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./);
expect(wrapper.find('[data-testid="move-license-to-tpm"]').exists()).toBe(true);
});

it('shows TPM purchase guidance instead of TPM transfer steps for trial states', async () => {
serverStore.state = 'TRIAL';
it('triggers the TPM replacement action when Move License to TPM is clicked', async () => {
serverStore.state = 'PRO';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.keyfile = 'keyfile-present';

await wrapper.vm.$nextTick();

const trialNotice = wrapper.find('[data-testid="tpm-transfer-trial"]');
await wrapper.find('[data-testid="move-license-to-tpm"]').trigger('click');

expect(trialNotice.exists()).toBe(true);
expect(trialNotice.text()).toContain(
'TPM licensing will be available after you purchase a license.'
);
expect(trialNotice.text()).toContain(
'Trial licenses cannot be moved to TPM. Once you purchase a license for this server, you will be able to transfer it from your USB flash device to TPM.'
);
expect(wrapper.find('[data-testid="tpm-transfer-available"]').exists()).toBe(false);
expect(accountStore.replaceTpm).toHaveBeenCalled();
});

it('shows checked TPM transfer steps after switching to TPM boot', async () => {
serverStore.state = 'EGUID';
serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.mdState = 'STOPPED';
it('does not show Move License to TPM for trial states', async () => {
serverStore.state = 'TRIAL';
serverStore.guid = '058F-6387-0000-0000F1F1E1C6';
serverStore.flashGuid = '058F-6387-0000-0000F1F1E1C6';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6';
serverStore.keyfile = 'keyfile-present';

await wrapper.vm.$nextTick();

const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]');

expect(transferNotice.exists()).toBe(true);
expect(transferNotice.text()).toContain('Continue your TPM license transfer.');
expect(transferNotice.text()).toContain('The first two steps are already complete.');
expect(transferNotice.text()).toContain('[x]');
expect(transferNotice.text()).toContain('Stop the array.');
expect(transferNotice.text()).toContain('Remove the USB flash boot device.');
expect(transferNotice.text()).toContain('Press Replace Key.');
expect(transferNotice.text()).toContain('Start the array.');
expect(wrapper.find('[data-testid="move-license-to-tpm"]').exists()).toBe(false);
});

it('shows the stop-array step as incomplete in TPM-ready state while the array is running', async () => {
it('does not show Move License to TPM after switching to TPM boot', async () => {
serverStore.state = 'EGUID';
serverStore.guid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.mdState = 'STARTED';
serverStore.tpmGuid = '03-V35H8S0L1QHK1SBG1XHXJNH7';
serverStore.regGuid = '058F-6387-0000-0000F1F1E1C6';

await wrapper.vm.$nextTick();

const transferNotice = wrapper.find('[data-testid="tpm-transfer-ready"]');

expect(transferNotice.exists()).toBe(true);
expect(transferNotice.text()).toContain(
'The USB flash boot device is already removed. Stop the array, then press Replace Key to transfer this license to TPM.'
);
expect(transferNotice.text()).toMatch(/\[\s\]Stop the array\./);
expect(transferNotice.text()).toMatch(/\[x\]Remove the USB flash boot device\./);
expect(wrapper.find('[data-testid="move-license-to-tpm"]').exists()).toBe(false);
});

it('adds Activate Trial fallback for ENOKEYFILE partner activation', async () => {
Expand Down
21 changes: 21 additions & 0 deletions web/__test__/store/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ vi.mock('~/store/server', () => ({
guid: 'test-guid',
name: 'test-server',
},
serverReplacePayload: {
guid: 'test-tpm-guid',
name: 'test-server',
},
inIframe: false,
}),
}));
Expand Down Expand Up @@ -218,6 +222,23 @@ describe('Account Store', () => {
);
});

it('should call replaceTpm action correctly', () => {
store.replaceTpm();

expect(mockSend).toHaveBeenCalledTimes(1);
expect(mockSend).toHaveBeenCalledWith(
ACCOUNT_CALLBACK.toString(),
[
{
server: { guid: 'test-tpm-guid', name: 'test-server' },
type: 'replace',
},
],
undefined,
'post'
);
});

it('should call trialExtend action correctly', () => {
store.trialExtend();

Expand Down
63 changes: 63 additions & 0 deletions web/__test__/store/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ const getStore = () => {
wanFQDN: store.wanFQDN,
}),
},
serverReplacePayload: {
get: () => ({
...store.serverAccountPayload,
guid:
store.guid && !store.guid.startsWith('03-') && store.tpmGuid && store.guid !== store.tpmGuid
? store.tpmGuid
: store.guid || undefined,
}),
},
});

// Mock store methods
Expand Down Expand Up @@ -618,6 +627,7 @@ describe('useServerStore', () => {
deviceCount: 6,
description: 'Test Server',
expireTime: 123,
flashGuid: 'flash-guid-1',
flashProduct: 'TestFlash',
flashVendor: 'TestVendor',
guid: '123456',
Expand Down Expand Up @@ -658,6 +668,59 @@ describe('useServerStore', () => {
expect(payload.wanFQDN).toBe('test.myunraid.net');
});

it('should create serverReplacePayload with TPM guid when available on flash boot', () => {
const store = getStore();

store.setServer({
flashGuid: '058F-6387-0000-0000F1F1E1C6',
guid: '058F-6387-0000-0000F1F1E1C6',
keyfile: '/boot/config/Pro.key',
state: 'PRO' as ServerState,
tpmGuid: '03-V35H8S0L1QHK1SBG1XHXJNH7',
});

expect(store.serverReplacePayload.guid).toBe('03-V35H8S0L1QHK1SBG1XHXJNH7');
});

it('should create serverReplacePayload with flash guid when TPM replacement is not available', () => {
const store = getStore();

store.setServer({
flashGuid: '058F-6387-0000-0000F1F1E1C6',
guid: '058F-6387-0000-0000F1F1E1C6',
state: 'PRO' as ServerState,
tpmGuid: '058F-6387-0000-0000F1F1E1C6',
});

expect(store.serverReplacePayload.guid).toBe('058F-6387-0000-0000F1F1E1C6');
});

it('should create serverReplacePayload with the active TPM guid when booted from TPM', () => {
const store = getStore();

store.setServer({
flashGuid: '058F-6387-0000-0000F1F1E1C6',
guid: '03-V35H8S0L1QHK1SBG1XHXJNH7',
state: 'PRO' as ServerState,
tpmGuid: undefined,
});

expect(store.serverReplacePayload.guid).toBe('03-V35H8S0L1QHK1SBG1XHXJNH7');
});

it('should create serverReplacePayload with the active flash guid when TPM guid is missing', () => {
const store = getStore();

store.setServer({
flashGuid: '058F-6387-0000-0000F1F1E1C6',
guid: '058F-6387-0000-0000F1F1E1C6',
state: 'PRO' as ServerState,
tpmGuid: undefined,
});

expect(store.serverReplacePayload.guid).toBe('058F-6387-0000-0000F1F1E1C6');
});

it('should handle OS version ignore functionality', () => {
const store = getStore();
store.setServer({ updateOsIgnoredReleases: [] });
Expand Down
7 changes: 1 addition & 6 deletions web/src/components/Onboarding/OnboardingModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,8 @@ const availableSteps = computed<StepId[]>(() => visibleHardcodedSteps.value.map(
// Filtered steps as full objects for OnboardingSteps component
const filteredSteps = computed(() => visibleHardcodedSteps.value);

const isLoginPage = computed(() => {
const hasLoginRoute = window.location.pathname.includes('login');
const hasLoginMarkup = Boolean(document.querySelector('#login, form[action="/login"]'));
return hasLoginRoute || hasLoginMarkup;
});
const showModal = computed(() => {
if (isLoginPage.value || !canDisplayOnboardingModal.value) {
if (!canDisplayOnboardingModal.value) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const {
isPartnerBuild,
completed,
completedAtVersion,
mockUnauthenticated,
osVersion,
effectiveOsVersion,
isVersionSupported,
Expand Down Expand Up @@ -376,11 +375,6 @@ const openOnboardingModalFromPanel = () => {
onboardingModalStore.forceOpenModal();
};

const onMockUnauthenticatedChange = (event: Event) => {
const target = event.target as HTMLInputElement | null;
onboardingStore.setMockUnauthenticated(Boolean(target?.checked));
};

const getMutationInput = (payload: OnboardingOverridePayload) => {
const mutationInput = { ...payload };
delete mutationInput.currentVersion;
Expand Down Expand Up @@ -767,26 +761,6 @@ const currentRegistrationState = computed({
</label>
</div>

<div class="border-border bg-background rounded-lg border p-3 shadow-sm">
<label class="flex cursor-pointer items-start gap-2">
<input
type="checkbox"
class="mt-0.5"
:checked="mockUnauthenticated"
@change="onMockUnauthenticatedChange"
/>
<div>
<div class="text-foreground text-xs font-semibold uppercase">
Mock Unauthenticated User
</div>
<div class="text-muted-foreground text-xs">
Simulates unauthenticated onboarding requests (401/CSRF) to verify onboarding modals
remain hidden. This setting persists in localStorage.
</div>
</div>
</label>
</div>

<div class="border-border bg-background rounded-lg border p-3 shadow-sm">
<label class="flex cursor-pointer items-start gap-2">
<input
Expand Down
Loading
Loading