diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts index 924dce1a0be9f..2a3f7f5321b5b 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/http_requests.ts @@ -7,11 +7,29 @@ import sinon, { SinonFakeServer } from 'sinon'; import { API_BASE_PATH } from '../../../common/constants'; -import { ESUpgradeStatus, DeprecationLoggingStatus } from '../../../common/types'; +import { + CloudBackupStatus, + ESUpgradeStatus, + DeprecationLoggingStatus, +} from '../../../common/types'; import { ResponseError } from '../../../public/application/lib/api'; // Register helpers to mock HTTP Requests const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { + const setLoadCloudBackupStatusResponse = ( + response?: CloudBackupStatus, + error?: ResponseError + ) => { + const status = error ? error.statusCode || 400 : 200; + const body = error ? error : response; + + server.respondWith('GET', `${API_BASE_PATH}/cloud_backup_status`, [ + status, + { 'Content-Type': 'application/json' }, + JSON.stringify(body), + ]); + }; + const setLoadEsDeprecationsResponse = (response?: ESUpgradeStatus, error?: ResponseError) => { const status = error ? error.statusCode || 400 : 200; const body = error ? error : response; @@ -109,6 +127,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { }; return { + setLoadCloudBackupStatusResponse, setLoadEsDeprecationsResponse, setLoadDeprecationLoggingResponse, setUpdateDeprecationLoggingResponse, @@ -131,8 +150,19 @@ export const init = () => { const httpRequestsMockHelpers = registerHttpRequestMockHelpers(server); + const setServerAsync = (isAsync: boolean, timeout: number = 200) => { + if (isAsync) { + server.autoRespond = true; + server.autoRespondAfter = 1000; + server.respondImmediately = false; + } else { + server.respondImmediately = true; + } + }; + return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index 2152b6adf8684..2cb44c1f2aa3a 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -17,9 +17,9 @@ import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; import { GlobalFlyout } from '../../../public/shared_imports'; +import { AppDependencies } from '../../../public/types'; import { getAppContextMock } from './app_context.mock'; import { init as initHttpRequests } from './http_requests'; -import { AppDependencies } from '../../../public/types'; const { GlobalFlyoutProvider } = GlobalFlyout; @@ -43,10 +43,11 @@ export const WithAppDependencies = (Comp: any, overrides: Record { - const { server, httpRequestsMockHelpers } = initHttpRequests(); + const { server, setServerAsync, httpRequestsMockHelpers } = initHttpRequests(); return { server, + setServerAsync, httpRequestsMockHelpers, }; }; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx index ab571790d56c6..c99aa7031aa3d 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/backup_step/backup_step.test.tsx @@ -5,27 +5,139 @@ * 2.0. */ +import { act } from 'react-dom/test-utils'; + import { setupEnvironment } from '../../helpers'; import { OverviewTestBed, setupOverviewPage } from '../overview.helpers'; describe('Overview - Backup Step', () => { let testBed: OverviewTestBed; - const { server } = setupEnvironment(); + let server: ReturnType['server']; + let setServerAsync: ReturnType['setServerAsync']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; - beforeEach(async () => { - testBed = await setupOverviewPage(); - testBed.component.update(); + beforeEach(() => { + ({ server, setServerAsync, httpRequestsMockHelpers } = setupEnvironment()); }); - afterAll(() => { + afterEach(() => { server.restore(); }); describe('On-prem', () => { + beforeEach(async () => { + testBed = await setupOverviewPage(); + }); + test('Shows link to Snapshot and Restore', () => { const { exists, find } = testBed; expect(exists('snapshotRestoreLink')).toBe(true); expect(find('snapshotRestoreLink').props().href).toBe('snapshotAndRestoreUrl'); }); }); + + describe('On Cloud', () => { + const setupCloudOverviewPage = async () => + setupOverviewPage({ + plugins: { + cloud: { + isCloudEnabled: true, + deploymentUrl: 'deploymentUrl', + }, + }, + }); + + describe('initial loading state', () => { + beforeEach(async () => { + // We don't want the request to load backup status to resolve immediately. + setServerAsync(true); + testBed = await setupCloudOverviewPage(); + }); + + afterEach(() => { + setServerAsync(false); + }); + + test('is rendered', () => { + const { exists } = testBed; + expect(exists('cloudBackupLoading')).toBe(true); + }); + }); + + describe('error state', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse(undefined, { + statusCode: 400, + message: 'error', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('is rendered', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupErrorCallout')).toBe(true); + }); + + test('lets the user attempt to reload backup status', () => { + const { exists } = testBed; + testBed.component.update(); + expect(exists('cloudBackupRetryButton')).toBe(true); + }); + }); + + describe('success state', () => { + describe('when data is backed up', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: true, + lastBackupTime: '2021-08-25T19:59:59.863Z', + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and last backup time ', () => { + const { exists, find } = testBed; + expect(exists('dataBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + expect(find('dataBackedUpStatus').text()).toContain('Last snapshot created on'); + }); + }); + + describe(`when data isn't backed up`, () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadCloudBackupStatusResponse({ + isBackedUp: false, + lastBackupTime: undefined, + }); + + testBed = await setupCloudOverviewPage(); + }); + + test('renders link to Cloud backups and "not backed up" status', () => { + const { exists } = testBed; + expect(exists('dataNotBackedUpStatus')).toBe(true); + expect(exists('cloudSnapshotsLink')).toBe(true); + }); + }); + + test('polls for new status', async () => { + // The behavior we're testing involves state changes over time, so we need finer control over + // timing. + jest.useFakeTimers(); + testBed = await setupCloudOverviewPage(); + expect(server.requests.length).toBe(4); + + // Resolve the polling timeout. + await act(async () => { + jest.runAllTimers(); + }); + + expect(server.requests.length).toBe(5); + jest.useRealTimers(); + }); + }); + }); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx index 2af9aa2e82702..e73cfaab8b2b6 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/backup_step/cloud_backup.tsx @@ -38,7 +38,7 @@ export const CloudBackup: React.FunctionComponent = ({ const { isInitialRequest, isLoading, error, data, resendRequest } = cloudBackupStatusResponse; if (isInitialRequest && isLoading) { - return ; + return ; } if (error) { @@ -66,7 +66,7 @@ export const CloudBackup: React.FunctionComponent = ({ const lastBackupTime = moment(data!.lastBackupTime).toISOString(); const statusMessage = data!.isBackedUp ? ( - + @@ -96,7 +96,7 @@ export const CloudBackup: React.FunctionComponent = ({ ) : ( - + diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts new file mode 100644 index 0000000000000..7095c115ae7e1 --- /dev/null +++ b/x-pack/test/api_integration/apis/upgrade_assistant/cloud_backup_status.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + const CLOUD_SNAPSHOT_REPOSITORY = 'found-snapshots'; + + const createCloudRepository = () => { + return es.snapshot + .createRepository({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + body: { + type: 'fs', + settings: { + location: '/tmp/cloud-snapshots/', + }, + }, + verify: false, + }) + .then(({ body }) => body); + }; + + const createCloudSnapshot = (snapshotName: string) => { + return es.snapshot.create({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + wait_for_completion: true, + // Configure snapshot so no indices are captured, so the request completes ASAP. + body: { + indices: 'this_index_doesnt_exist', + ignore_unavailable: true, + include_global_state: false, + }, + }); + }; + + const deleteCloudSnapshot = (snapshotName: string) => { + return es.snapshot.delete({ + repository: CLOUD_SNAPSHOT_REPOSITORY, + snapshot: snapshotName, + }); + }; + + describe('Cloud backup status', () => { + describe('get', () => { + describe('with backups present', () => { + // Needs SnapshotInfo type https://github.com/elastic/elasticsearch-specification/issues/685 + let mostRecentSnapshot: any; + + before(async () => { + await createCloudRepository(); + await createCloudSnapshot('test_snapshot_1'); + mostRecentSnapshot = (await createCloudSnapshot('test_snapshot_2')).body.snapshot; + }); + + after(async () => { + await deleteCloudSnapshot('test_snapshot_1'); + await deleteCloudSnapshot('test_snapshot_2'); + }); + + it('returns status based on most recent snapshot', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(true); + expect(cloudBackupStatus.lastBackupTime).to.be(mostRecentSnapshot.start_time); + }); + }); + + describe('without backups present', () => { + it('returns not-backed-up status', async () => { + const { body: cloudBackupStatus } = await supertest + .get('/api/upgrade_assistant/cloud_backup_status') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(cloudBackupStatus.isBackedUp).to.be(false); + expect(cloudBackupStatus.lastBackupTime).to.be(undefined); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts index 466d44ca460ac..ddb71c59423ba 100644 --- a/x-pack/test/api_integration/apis/upgrade_assistant/index.ts +++ b/x-pack/test/api_integration/apis/upgrade_assistant/index.ts @@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Upgrade Assistant', () => { loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./cloud_backup_status')); }); }