Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common';

export const FLIGHTS_DATASET_ID = 'flights';

export const FLIGHTS_ES_INDEX = 'kibana_sample_data_flights';

// @see src/platform/plugins/shared/home/server/services/sample_data/data_sets/flights/index.ts
export const FLIGHTS_OVERVIEW_DASHBOARD_ID = '7adfa750-4c81-11e8-b3d7-01146121b73d';

export const COMMON_HEADERS = {
'kbn-xsrf': 'kibana',
[X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'kibana',
} as const;
Comment thread
paulinashakirova marked this conversation as resolved.

// Earliest-to-latest timestamp delta in the flights dataset; used to verify timestamps shift relative to install time.
// @see src/platform/plugins/shared/home/server/services/sample_data/data_sets/flights/flights.json.gz
export const FLIGHTS_DATA_TIME_SPAN_MS =
new Date('2018-02-11T14:54:34').getTime() - new Date('2018-01-01T00:00:00').getTime();
Comment thread
paulinashakirova marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { ScoutTestFixtures, ScoutWorkerFixtures } from '@kbn/scout';
import { apiTest as baseApiTest } from '@kbn/scout';

export const apiTest = baseApiTest.extend<ScoutTestFixtures, ScoutWorkerFixtures>({});

export {
FLIGHTS_DATASET_ID,
FLIGHTS_ES_INDEX,
FLIGHTS_OVERVIEW_DASHBOARD_ID,
COMMON_HEADERS,
FLIGHTS_DATA_TIME_SPAN_MS,
} from './constants';
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { FtrProviderContext } from '../../ftr_provider_context';
import { createPlaywrightConfig } from '@kbn/scout';

export default function ({ loadTestFile }: FtrProviderContext) {
describe('home apis', () => {
loadTestFile(require.resolve('./sample_data'));
});
}
export default createPlaywrightConfig({
testDir: './tests',
workers: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { randomUUID } from 'crypto';
import type { RoleApiCredentials } from '@kbn/scout';
import { tags } from '@kbn/scout';
import { expect } from '@kbn/scout/api';
import {
apiTest,
FLIGHTS_DATASET_ID,
FLIGHTS_ES_INDEX,
FLIGHTS_OVERVIEW_DASHBOARD_ID,
FLIGHTS_DATA_TIME_SPAN_MS,
COMMON_HEADERS,
} from '../fixtures';

// UUID suffix keeps saved objects isolated to their own space across concurrent CI runs.
const TEST_SPACE_ID = `scout-sd-${randomUUID().slice(0, 8)}`;

const sampleDataApiPath = (space: string) => `/s/${space}/api/sample_data`;

apiTest.describe('sample data API', { tag: tags.stateful.classic }, () => {
let credentials: RoleApiCredentials;

apiTest.beforeAll(async ({ requestAuth, kbnClient }) => {
credentials = await requestAuth.getApiKeyForAdmin();

await kbnClient.spaces.create({ id: TEST_SPACE_ID, name: 'Scout sample data test space' });
});

apiTest.afterAll(async ({ kbnClient }) => {
await kbnClient.spaces.delete(TEST_SPACE_ID);
});
Comment thread
paulinashakirova marked this conversation as resolved.

// ---------------------------------------------------------------------------
// Negative path
// ---------------------------------------------------------------------------

apiTest('returns 404 for an unknown dataset ID', async ({ apiClient }) => {
const response = await apiClient.post('/api/sample_data/xxxx', {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
});
Comment thread
paulinashakirova marked this conversation as resolved.

expect(response).toHaveStatusCode(404);
});

// ---------------------------------------------------------------------------
// Default space: full lifecycle
// ---------------------------------------------------------------------------

apiTest(
'default space: list, install, verify timestamps and counts, uninstall',
async ({ apiClient, esClient }) => {
const apiPath = sampleDataApiPath('default');

await apiTest.step(
'install returns 200 with correct ES index and saved object counts',
async () => {
const response = await apiClient.post(`${apiPath}/${FLIGHTS_DATASET_ID}`, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
expect(response.body).toStrictEqual({
elasticsearchIndicesCreated: { [FLIGHTS_ES_INDEX]: 13014 },
kibanaSavedObjectsLoaded: 7,
});
}
);

await apiTest.step(
'ES index contains timestamps relative to current time (no ?now= param)',
async () => {
const result = await esClient.search<{ timestamp: string }>({
index: FLIGHTS_ES_INDEX,
sort: [{ timestamp: { order: 'desc' } }],
});
const latestTimestampMs = Date.parse(result.hits.hits[0]._source!.timestamp);
const delta = Math.abs(Date.now() - latestTimestampMs);
expect(delta).toBeLessThan(FLIGHTS_DATA_TIME_SPAN_MS);
}
);

await apiTest.step(
'ES index timestamps shift to match the ?now= parameter on reinstall',
async () => {
const nowString = '2000-01-01T00:00:00';
const reinstallResponse = await apiClient.post(
`${apiPath}/${FLIGHTS_DATASET_ID}?now=${encodeURIComponent(nowString)}`,
{ headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader } }
);
Comment thread
paulinashakirova marked this conversation as resolved.
Comment thread
paulinashakirova marked this conversation as resolved.
expect(reinstallResponse).toHaveStatusCode(200);
const result = await esClient.search<{ timestamp: string }>({
index: FLIGHTS_ES_INDEX,
sort: [{ timestamp: { order: 'desc' } }],
});
const latestTimestampMs = Date.parse(result.hits.hits[0]._source!.timestamp);
const delta = Math.abs(Date.parse(nowString) - latestTimestampMs);
expect(delta).toBeLessThan(FLIGHTS_DATA_TIME_SPAN_MS);
}
);

await apiTest.step('list returns installed status after install', async () => {
const response = await apiClient.get(apiPath, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
const flights = findFlightsDataset(response.body);
expect(flights.status).toBe('installed');
expect(flights.overviewDashboard).toBe(FLIGHTS_OVERVIEW_DASHBOARD_ID);
});

await apiTest.step('uninstall returns 204', async () => {
const response = await apiClient.delete(`${apiPath}/${FLIGHTS_DATASET_ID}`, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
});
expect(response).toHaveStatusCode(204);
});

await apiTest.step('ES index is removed after uninstall', async () => {
const indexExists = await esClient.indices.exists({ index: FLIGHTS_ES_INDEX });
expect(indexExists).toBe(false);
});

await apiTest.step('list returns not_installed status after uninstall', async () => {
const response = await apiClient.get(apiPath, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
const flights = findFlightsDataset(response.body);
expect(flights.status).toBe('not_installed');
expect(flights.overviewDashboard).toBe(FLIGHTS_OVERVIEW_DASHBOARD_ID);
});
}
);

// ---------------------------------------------------------------------------
// Non-default space: ID preservation + full lifecycle
// ---------------------------------------------------------------------------
Comment thread
paulinashakirova marked this conversation as resolved.

apiTest(
'non-default space: install and uninstall work correctly; saved object IDs are preserved',
async ({ apiClient, esClient }) => {
const apiPath = sampleDataApiPath(TEST_SPACE_ID);
Comment thread
paulinashakirova marked this conversation as resolved.

await apiTest.step(
'install returns 200 with correct ES index and saved object counts',
async () => {
const response = await apiClient.post(`${apiPath}/${FLIGHTS_DATASET_ID}`, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
expect(response.body).toStrictEqual({
elasticsearchIndicesCreated: { [FLIGHTS_ES_INDEX]: 13014 },
kibanaSavedObjectsLoaded: 7,
});
}
);

await apiTest.step(
'list shows installed status and the canonical overviewDashboard ID in non-default space',
async () => {
const response = await apiClient.get(apiPath, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
const flights = findFlightsDataset(response.body);
expect(flights.status).toBe('installed');
// The installer uses createNewCopies: false, so saved object IDs are identical across all spaces.
expect(flights.overviewDashboard).toBe(FLIGHTS_OVERVIEW_DASHBOARD_ID);
}
);

await apiTest.step('uninstall returns 204', async () => {
const response = await apiClient.delete(`${apiPath}/${FLIGHTS_DATASET_ID}`, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
});
expect(response).toHaveStatusCode(204);
});

await apiTest.step('ES index is removed after uninstall', async () => {
const indexExists = await esClient.indices.exists({ index: FLIGHTS_ES_INDEX });
expect(indexExists).toBe(false);
});

await apiTest.step('list returns not_installed status after uninstall', async () => {
const response = await apiClient.get(apiPath, {
headers: { ...COMMON_HEADERS, ...credentials.apiKeyHeader },
responseType: 'json',
});
expect(response).toHaveStatusCode(200);
const flights = findFlightsDataset(response.body);
expect(flights.status).toBe('not_installed');
expect(flights.overviewDashboard).toBe(FLIGHTS_OVERVIEW_DASHBOARD_ID);
});
}
);
});

function findFlightsDataset(
body: Array<{ id: string; status: string; overviewDashboard: string }>
) {
const dataset = body.find(({ id }) => id === FLIGHTS_DATASET_ID);
if (!dataset) {
throw new Error(`"${FLIGHTS_DATASET_ID}" dataset not found in sample data list response`);
}
return dataset;
}
Loading
Loading