diff --git a/pkg/rancher-prime/l10n/en-us.yaml b/pkg/rancher-prime/l10n/en-us.yaml index 649e3cdad0b..b9627ac4588 100644 --- a/pkg/rancher-prime/l10n/en-us.yaml +++ b/pkg/rancher-prime/l10n/en-us.yaml @@ -55,6 +55,11 @@ registration: invalid: Invalid expired: Expired valid: Active + errors: + missing-code: Registration code as Secret is missing + mismatch-code: Registration active but does not match Secret registration code. Please verify the registration + generic-registration: No registration found for existing registration code (Secret). + timeout-registration: Timeout reached while waiting for resource. prime: installed: This is a SUSE Rancher Prime installation diff --git a/pkg/rancher-prime/pages/registration.composable.test.ts b/pkg/rancher-prime/pages/registration.composable.test.ts index 00f0f347221..6fc26afc7fb 100644 --- a/pkg/rancher-prime/pages/registration.composable.test.ts +++ b/pkg/rancher-prime/pages/registration.composable.test.ts @@ -19,7 +19,7 @@ describe('registration composable', () => { describe('when initialized', () => { it('should retrieve the registration secret and current registration', async() => { - const value = 'whatever'; + const regCode = 'whatever'; const hash = 'anything'; const secrets = [ { metadata: { namespace: 'not me' } }, @@ -30,7 +30,7 @@ describe('registration composable', () => { name: REGISTRATION_SECRET, labels: { [REGISTRATION_LABEL]: hash } }, - data: { regCode: btoa(value) } + data: { regCode: btoa(regCode) } }, ]; const registrations = [{ @@ -54,14 +54,14 @@ describe('registration composable', () => { await initRegistration(); jest.setTimeout(0); - expect(registrationCode.value).toStrictEqual(value); + expect(registrationCode.value).toStrictEqual(regCode); expect(registration.value.active).toStrictEqual(true); expect(registration.value.resourceLink).toStrictEqual('123'); expect(registrationStatus.value).toStrictEqual('registered'); }); it('should display the correct error message, prioritizing conditions without Failure', async() => { - const value = 'whatever'; + const regCode = 'whatever'; const hash = 'anything'; const errorMessage = 'Registration failed'; const secrets = [ @@ -73,7 +73,7 @@ describe('registration composable', () => { name: REGISTRATION_SECRET, labels: { [REGISTRATION_LABEL]: hash } }, - data: { regCode: btoa(value) } + data: { regCode: btoa(regCode) } }, ]; const registrations = [{ @@ -315,7 +315,7 @@ describe('registration composable', () => { }); describe('deregistering', () => { - it('should reset all de values', async() => { + it('should reset all the values', async() => { const { registerOffline, registrationStatus @@ -326,4 +326,63 @@ describe('registration composable', () => { expect(registrationStatus.value).toStrictEqual('registering-offline'); }); }); + + describe('should display an error message', () => { + it('with generic error if Registration Code is present but not active Registration is found', async() => { + const expectation = 'registration.errors.generic-registration'; + const regCode = 'whatever'; + const hash = 'anything'; + const secrets = [{ + metadata: { + namespace: REGISTRATION_NAMESPACE, + name: REGISTRATION_SECRET, + labels: { [REGISTRATION_LABEL]: hash } + }, + data: { regCode: btoa(regCode) } + }, + ]; + const registrations = [] as any[]; + + dispatchSpy = jest.fn() + .mockReturnValueOnce(Promise.resolve(secrets)) + .mockReturnValue(Promise.resolve(registrations)); + const store = { state: {}, dispatch: dispatchSpy } as any; + const { initRegistration, errors } = usePrimeRegistration(store); + + await initRegistration(); + jest.setTimeout(0); + + expect(errors.value[0]).toStrictEqual(expectation); + }); + + describe('registering online', () => { + it.skip('given no registration code', async() => { + const expectation = 'registration.errors.missing-code'; + const store = { state: {}, dispatch: dispatchSpy } as any; + const { errors } = usePrimeRegistration(store); + + expect(errors.value[0]).toStrictEqual(expectation); + }); + + it.skip('given a mismatched registration code', async() => { + const expectation = 'registration.errors.mismatch-code'; + const store = { state: {}, dispatch: dispatchSpy } as any; + const { errors } = usePrimeRegistration(store); + + expect(errors.value[0]).toStrictEqual(expectation); + }); + + it.skip('given no response', async() => { + const expectation = 'registration.errors.timeout-registration'; + const store = { state: {}, dispatch: dispatchSpy } as any; + const { errors, registerOnline, registrationCode } = usePrimeRegistration(store); + + registrationCode.value = 'not a real code'; + + await registerOnline((val: boolean) => true); + + expect(errors.value[0]).toStrictEqual(expectation); + }); + }); + }); }); diff --git a/pkg/rancher-prime/pages/registration.composable.ts b/pkg/rancher-prime/pages/registration.composable.ts index 19fbd873ac1..e5501eb1e45 100644 --- a/pkg/rancher-prime/pages/registration.composable.ts +++ b/pkg/rancher-prime/pages/registration.composable.ts @@ -8,6 +8,7 @@ import { } from '../config/constants'; import { SECRET } from '@shell/config/types'; import { dateTimeFormat } from '@shell/utils/time'; +import { useI18n } from '@shell/composables/useI18n'; type RegistrationStatus = 'loading' | 'registering-online' | 'registration-request' | 'registering-offline' | 'registered' | null; type AsyncButtonFunction = (val: boolean) => void; @@ -21,6 +22,7 @@ interface RegistrationDashboard { color: 'error' | 'success'; message: string; status: 'valid' | 'error' | 'none'; + code: string | null; registrationLink?: string; // not generated on failure or reset resourceLink?: string; // not generated on empty registration } @@ -89,6 +91,7 @@ const emptyRegistration: RegistrationDashboard = { mode: '--', expiration: '--', color: 'error', + code: null, message: 'registration.list.table.badge.none', status: 'none' }; @@ -110,6 +113,7 @@ const registrationBannerCases = { export const usePrimeRegistration = (storeArg?: Store) => { const store = storeArg ?? useStore(); + const { t } = useI18n(store); /** * Registration mapped value used in the UI @@ -338,12 +342,12 @@ export const usePrimeRegistration = (storeArg?: Store) => { */ const findRegistration = async(hash: string | null): Promise => { const registrations: PartialRegistration[] = await store.dispatch('management/findAll', { type: REGISTRATION_RESOURCE_NAME }).catch(() => []) || []; - const registration = registrations.find((registration) => registration.metadata?.labels[REGISTRATION_LABEL] === hash && + const newRegistration = registrations.find((registration) => registration.metadata?.labels[REGISTRATION_LABEL] === hash && !isRegistrationOfflineProgress(registration) && isRegistrationCompleted(registration) ); - return registration; + return newRegistration; }; /** @@ -381,6 +385,7 @@ export const usePrimeRegistration = (storeArg?: Store) => { mode: registration.spec.mode, registrationLink: registration.status?.activationStatus?.systemUrl, resourceLink: registration.links.view, + code: registration?.metadata?.labels[REGISTRATION_LABEL], }; if (isActive) { @@ -399,7 +404,7 @@ export const usePrimeRegistration = (storeArg?: Store) => { if (errorMessage) { onError(errorMessage); } else { - onError(new Error('Registration failed without a specific error message')); + onError(new Error(t('registration.errors.generic-registration'))); } return { @@ -480,6 +485,32 @@ export const usePrimeRegistration = (storeArg?: Store) => { await secret.save(); }; + /** + * Generic fallback in case of unhandled errors based on existing resources + * @param polling + * @returns + */ + const getError = (polling?: boolean): string => { + if (polling && !secret.value?.data?.regCode) { + return t('registration.errors.missing-code'); + } + + // Fallback in case of logic changes + if (polling && registration.value.active && secret.value?.data?.regCode !== registration.value?.code) { + return t('registration.errors.mismatch-code'); + } + + if (secret.value && !registration.value.active) { + return t('registration.errors.generic-registration'); + } + + if (polling) { + return t('registration.errors.timeout-registration'); + } + + return ''; + }; + /** * Polls periodically until a condition is met or timeout is reached. * @param fetchFn Function to fetch the resource (e.g., findRegistration or findOfflineRequest) @@ -495,7 +526,7 @@ export const usePrimeRegistration = (storeArg?: Store) => { mapResult: (resource: any) => T, extraConditionFn?: (resource: any) => boolean, frequency = 250, - timeout = 10000 + timeout = 10000 // First initialization is slow, which is most of the cases ): Promise => { return new Promise((resolve, reject) => { const startTime = Date.now(); @@ -503,7 +534,7 @@ export const usePrimeRegistration = (storeArg?: Store) => { const interval = setInterval(async() => { if (Date.now() - startTime > timeout) { clearInterval(interval); - reject(new Error('Timeout reached while waiting for resource')); + reject(new Error(getError(true))); return; } @@ -532,6 +563,11 @@ export const usePrimeRegistration = (storeArg?: Store) => { secret.value = await getSecret(); registrationCode.value = secret.value?.data?.regCode ? atob(secret.value.data.regCode) : null; // Get registration code from secret registrationStatus.value = await getRegistration(); + const message = getError(); + + if (message) { + onError(new Error(message)); + } }; return {