-
Notifications
You must be signed in to change notification settings - Fork 19
web: warn before risky internal-boot downgrade from 7.3.x #1933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import { mount } from '@vue/test-utils'; | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import UpdateOsDowngrade from '~/components/UpdateOs/Downgrade.vue'; | ||
| import { createTestI18n } from '../utils/i18n'; | ||
|
|
||
| const mockViewReleaseNotes = vi.fn(); | ||
| const mockServerStore = { | ||
| bootedFromFlashWithInternalBootSetup: false, | ||
| dateTimeFormat: { date: '%A, %m/%d/%Y', time: '%R' }, | ||
| osVersion: '7.3.0', | ||
| }; | ||
|
|
||
| vi.mock('pinia', async (importOriginal) => { | ||
| const actual = (await importOriginal()) as typeof import('pinia'); | ||
| return { | ||
| ...actual, | ||
| storeToRefs: (store: Record<string, unknown>) => | ||
| Object.fromEntries( | ||
| Object.keys(store).map((key) => [ | ||
| key, | ||
| { | ||
| get value() { | ||
| return store[key]; | ||
| }, | ||
| set value(value: unknown) { | ||
| store[key] = value; | ||
| }, | ||
| }, | ||
| ]) | ||
| ), | ||
| }; | ||
| }); | ||
|
|
||
| vi.mock('@unraid/ui', () => ({ | ||
| BrandButton: { | ||
| template: | ||
| '<button :data-button-name="name" :data-button-text="text" @click="$emit(\'click\')">{{ text }}</button>', | ||
| emits: ['click'], | ||
| props: ['name', 'text'], | ||
| }, | ||
| CardWrapper: { | ||
| template: '<div><slot /></div>', | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock('~/helpers/urls', async (importOriginal) => { | ||
| const actual = await importOriginal<typeof import('~/helpers/urls')>(); | ||
| return { | ||
| ...actual, | ||
| FORUMS_BUG_REPORT: new URL('https://example.com/bug-report'), | ||
| }; | ||
| }); | ||
|
|
||
| vi.mock('~/store/server', () => ({ | ||
| useServerStore: () => mockServerStore, | ||
| })); | ||
|
|
||
| vi.mock('~/store/updateOsActions', () => ({ | ||
| useUpdateOsActionsStore: () => ({ | ||
| viewReleaseNotes: mockViewReleaseNotes, | ||
| }), | ||
| })); | ||
|
|
||
| describe('UpdateOs/Downgrade', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockServerStore.bootedFromFlashWithInternalBootSetup = false; | ||
| mockServerStore.dateTimeFormat = { date: '%A, %m/%d/%Y', time: '%R' }; | ||
| mockServerStore.osVersion = '7.3.0'; | ||
| (window as Window & { confirmDowngrade?: () => void }).confirmDowngrade = vi.fn(); | ||
| }); | ||
|
|
||
| const mountComponent = (version = '7.2.5') => | ||
| mount(UpdateOsDowngrade, { | ||
| props: { | ||
| releaseDate: '2024-01-01', | ||
| version, | ||
| }, | ||
| global: { | ||
| plugins: [createTestI18n()], | ||
| }, | ||
| }); | ||
|
|
||
| it('starts downgrade immediately when warning conditions are not met', async () => { | ||
| mockServerStore.osVersion = '7.2.9'; | ||
| mockServerStore.bootedFromFlashWithInternalBootSetup = true; | ||
|
|
||
| const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); | ||
| const wrapper = mountComponent('7.2.8'); | ||
|
|
||
| await wrapper.find('[data-button-name="downgrade"]').trigger('click'); | ||
|
|
||
| expect(confirmSpy).not.toHaveBeenCalled(); | ||
| expect(window.confirmDowngrade).toHaveBeenCalledTimes(1); | ||
| expect(wrapper.text()).not.toContain('Internal boot downgrade risk'); | ||
| }); | ||
|
|
||
| it('shows warning and blocks downgrade when user cancels confirmation', async () => { | ||
| mockServerStore.osVersion = '7.3.0-beta.2'; | ||
| mockServerStore.bootedFromFlashWithInternalBootSetup = true; | ||
|
|
||
| const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false); | ||
| const wrapper = mountComponent('7.2.3'); | ||
|
|
||
| await wrapper.find('[data-button-name="downgrade"]').trigger('click'); | ||
|
|
||
| expect(confirmSpy).toHaveBeenCalledTimes(1); | ||
| expect(window.confirmDowngrade).not.toHaveBeenCalled(); | ||
| expect(wrapper.text()).toContain('Internal boot downgrade risk'); | ||
| }); | ||
|
|
||
| it('shows warning and allows downgrade after explicit confirmation', async () => { | ||
| mockServerStore.osVersion = '7.3.1'; | ||
| mockServerStore.bootedFromFlashWithInternalBootSetup = true; | ||
|
|
||
| const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true); | ||
| const wrapper = mountComponent('7.1.0'); | ||
|
|
||
| await wrapper.find('[data-button-name="downgrade"]').trigger('click'); | ||
|
|
||
| expect(confirmSpy).toHaveBeenCalledTimes(1); | ||
| expect(window.confirmDowngrade).toHaveBeenCalledTimes(1); | ||
| expect(wrapper.text()).toContain('Internal boot downgrade risk'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| <script lang="ts" setup> | ||
| import { ref } from 'vue'; | ||
| import { computed, ref } from 'vue'; | ||
| import { useI18n } from 'vue-i18n'; | ||
| import { storeToRefs } from 'pinia'; | ||
|
|
||
|
|
@@ -13,6 +13,8 @@ import { | |
| import { BrandButton, CardWrapper } from '@unraid/ui'; | ||
| import { FORUMS_BUG_REPORT } from '~/helpers/urls'; | ||
| import dayjs from 'dayjs'; | ||
| import coerce from 'semver/functions/coerce'; | ||
| import lt from 'semver/functions/lt'; | ||
|
|
||
| import type { UserProfileLink } from '~/types/userProfile'; | ||
|
|
||
|
|
@@ -29,14 +31,51 @@ const { t } = useI18n(); | |
| const serverStore = useServerStore(); | ||
| const updateOsActionsStore = useUpdateOsActionsStore(); | ||
|
|
||
| const { dateTimeFormat } = storeToRefs(serverStore); | ||
| const { bootedFromFlashWithInternalBootSetup, dateTimeFormat, osVersion } = storeToRefs(serverStore); | ||
| const { outputDateTimeFormatted: formattedReleaseDate } = useDateTimeHelper( | ||
| dateTimeFormat.value, | ||
| t, | ||
| true, | ||
| dayjs(props.releaseDate, 'YYYY-MM-DD').valueOf() | ||
| ); | ||
|
|
||
| const INTERNAL_BOOT_SUPPORT_VERSION = '7.3.0'; | ||
|
|
||
| const isVersionIn73Series = (version: string | null | undefined) => { | ||
| const normalizedVersion = coerce(version); | ||
| if (!normalizedVersion) { | ||
| return false; | ||
| } | ||
| return normalizedVersion.major === 7 && normalizedVersion.minor === 3; | ||
| }; | ||
|
|
||
| const isBeforeInternalBootSupportVersion = (version: string | null | undefined) => { | ||
| const normalizedVersion = coerce(version); | ||
| if (!normalizedVersion) { | ||
| return false; | ||
| } | ||
| return lt(normalizedVersion, INTERNAL_BOOT_SUPPORT_VERSION); | ||
| }; | ||
|
|
||
| const shouldWarnAboutInternalBootDowngrade = computed( | ||
| () => | ||
| bootedFromFlashWithInternalBootSetup.value && | ||
| isVersionIn73Series(osVersion.value) && | ||
| isBeforeInternalBootSupportVersion(props.version) | ||
|
Comment on lines
+62
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| ); | ||
|
|
||
| const startDowngrade = () => { | ||
| if (shouldWarnAboutInternalBootDowngrade.value) { | ||
| const confirmed = window.confirm( | ||
| t('updateOs.downgrade.internalBootConfirmFrom73xToOlder', [osVersion.value, props.version]) | ||
| ); | ||
| if (!confirmed) { | ||
| return; | ||
| } | ||
| } | ||
| window.confirmDowngrade?.(); | ||
| }; | ||
|
|
||
| const diagnosticsButton = ref<UserProfileLink | undefined>({ | ||
| click: () => { | ||
| window.downloadDiagnostics?.(); | ||
|
|
@@ -47,9 +86,7 @@ const diagnosticsButton = ref<UserProfileLink | undefined>({ | |
| }); | ||
|
|
||
| const downgradeButton = ref<UserProfileLink>({ | ||
| click: () => { | ||
| window.confirmDowngrade?.(); | ||
| }, | ||
| click: startDowngrade, | ||
| name: 'downgrade', | ||
| text: t('updateOs.downgrade.beginDowngradeTo', [props.version]), | ||
| }); | ||
|
|
@@ -82,6 +119,17 @@ const downgradeButton = ref<UserProfileLink>({ | |
| {{ t('updateOs.downgrade.downloadTheDiagnosticsZipThenPlease') }} | ||
| </p> | ||
| </div> | ||
| <div | ||
| v-if="shouldWarnAboutInternalBootDowngrade" | ||
| class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm leading-relaxed text-red-900" | ||
| > | ||
| <p class="font-semibold"> | ||
| {{ t('updateOs.downgrade.internalBootWarningTitle') }} | ||
| </p> | ||
| <p> | ||
| {{ t('updateOs.downgrade.internalBootWarningFrom73xToOlder', [osVersion, version]) }} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div v-if="downgradeButton" class="flex shrink-0 grow flex-col items-stretch gap-4"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition only fires when
bootedFromFlashWithInternalBootSetupis true, but the onboarding flow treatsbootedFromFlashWithInternalBootSetup === falseas the state where the machine is already booting internally (web/src/components/Onboarding/OnboardingModal.vue:84-92, plus the “already booting internally” test inweb/__test__/components/Onboarding/OnboardingModal.test.ts:369-380). In that common post-migration state, downgrading from 7.3.x to a pre-7.3 release is still the same boot-breaking case, yet this PR suppresses both the red warning and the extra confirmation.Useful? React with 👍 / 👎.