diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index a453b0bbae2fb..7d86105fed7fd 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -55,6 +55,7 @@ export async function matchEntriesWithExctractors(inputPath, options = {}) { '**/dist/**', '**/target/**', '**/vendor/**', + '**/build/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.d.ts', ].concat(additionalIgnore); diff --git a/src/dev/i18n/tasks/extract_untracked_translations.ts b/src/dev/i18n/tasks/extract_untracked_translations.ts index 7afaa1ef71a06..1455a9a00f766 100644 --- a/src/dev/i18n/tasks/extract_untracked_translations.ts +++ b/src/dev/i18n/tasks/extract_untracked_translations.ts @@ -38,7 +38,6 @@ export async function extractUntrackedMessagesTask({ '**/packages/kbn-i18n/**', '**/packages/kbn-i18n-react/**', '**/packages/kbn-plugin-generator/template/**', - '**/target/**', '**/test/**', '**/scripts/**', '**/src/dev/**', diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts new file mode 100644 index 0000000000000..736367446d3c0 --- /dev/null +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts @@ -0,0 +1,103 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { registerTelemetryUsageStatsRoutes } from './telemetry_usage_stats'; +import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import type { RequestHandlerContext, IRouter } from 'kibana/server'; +import { telemetryCollectionManagerPluginMock } from '../../../telemetry_collection_manager/server/mocks'; + +async function runRequest( + mockRouter: IRouter, + body?: { unencrypted?: boolean; refreshCache?: boolean } +) { + expect(mockRouter.post).toBeCalled(); + const [, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; + const mockResponse = httpServerMock.createResponseFactory(); + const mockRequest = httpServerMock.createKibanaRequest({ body }); + await handler(null, mockRequest, mockResponse); + + return { mockResponse, mockRequest }; +} + +describe('registerTelemetryUsageStatsRoutes', () => { + const router = { + handler: undefined, + config: undefined, + post: jest.fn().mockImplementation((config, handler) => { + router.config = config; + router.handler = handler; + }), + }; + const telemetryCollectionManager = telemetryCollectionManagerPluginMock.createSetupContract(); + const mockCoreSetup = coreMock.createSetup(); + const mockRouter = mockCoreSetup.http.createRouter(); + const mockStats = [{ clusterUuid: 'text', stats: 'enc_str' }]; + telemetryCollectionManager.getStats.mockResolvedValue(mockStats); + + describe('clusters/_stats POST route', () => { + it('registers _stats POST route and accepts body configs', () => { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + expect(mockRouter.post).toBeCalledTimes(1); + const [routeConfig, handler] = (mockRouter.post as jest.Mock).mock.calls[0]; + expect(routeConfig.path).toMatchInlineSnapshot(`"/api/telemetry/v2/clusters/_stats"`); + expect(Object.keys(routeConfig.validate.body.props)).toEqual(['unencrypted', 'refreshCache']); + expect(handler).toBeInstanceOf(Function); + }); + + it('responds with encrypted stats with no cache refresh by default', async () => { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + + const { mockRequest, mockResponse } = await runRequest(mockRouter); + expect(telemetryCollectionManager.getStats).toBeCalledWith({ + request: mockRequest, + unencrypted: undefined, + refreshCache: undefined, + }); + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]).toEqual({ body: mockStats }); + }); + + it('when unencrypted is set getStats is called with unencrypted and refreshCache', async () => { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + + const { mockRequest } = await runRequest(mockRouter, { unencrypted: true }); + expect(telemetryCollectionManager.getStats).toBeCalledWith({ + request: mockRequest, + unencrypted: true, + refreshCache: true, + }); + }); + + it('calls getStats with refreshCache when set in body', async () => { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + const { mockRequest } = await runRequest(mockRouter, { refreshCache: true }); + expect(telemetryCollectionManager.getStats).toBeCalledWith({ + request: mockRequest, + unencrypted: undefined, + refreshCache: true, + }); + }); + + it('calls getStats with refreshCache:true even if set to false in body when unencrypted is set to true', async () => { + registerTelemetryUsageStatsRoutes(mockRouter, telemetryCollectionManager, true); + const { mockRequest } = await runRequest(mockRouter, { + refreshCache: false, + unencrypted: true, + }); + expect(telemetryCollectionManager.getStats).toBeCalledWith({ + request: mockRequest, + unencrypted: true, + refreshCache: true, + }); + }); + + it.todo('always returns an empty array on errors on encrypted payload'); + it.todo('returns the actual request error object when in development mode'); + it.todo('returns forbidden on unencrypted and ES returns 403 in getStats'); + }); +}); diff --git a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts index e3ce8cbc5190a..2f72ae818f112 100644 --- a/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts +++ b/src/plugins/telemetry/server/routes/telemetry_usage_stats.ts @@ -35,7 +35,7 @@ export function registerTelemetryUsageStatsRoutes( const statsConfig: StatsGetterConfig = { request: req, unencrypted, - refreshCache, + refreshCache: unencrypted || refreshCache, }; const stats = await telemetryCollectionManager.getStats(statsConfig); diff --git a/src/plugins/telemetry_collection_manager/server/mocks.ts b/src/plugins/telemetry_collection_manager/server/mocks.ts new file mode 100644 index 0000000000000..cbd9dac9cfaa6 --- /dev/null +++ b/src/plugins/telemetry_collection_manager/server/mocks.ts @@ -0,0 +1,39 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { + TelemetryCollectionManagerPluginSetup, + TelemetryCollectionManagerPluginStart, +} from './types'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +export const telemetryCollectionManagerPluginMock = { + createSetupContract, + createStartContract, +}; + +function createSetupContract(): Setup { + const setupContract: Setup = { + getStats: jest.fn(), + getOptInStats: jest.fn(), + setCollectionStrategy: jest.fn(), + }; + + return setupContract; +} + +function createStartContract(): Start { + const startContract: Start = { + getOptInStats: jest.fn(), + getStats: jest.fn(), + }; + + return startContract; +} diff --git a/src/plugins/telemetry_collection_manager/server/plugin.test.ts b/src/plugins/telemetry_collection_manager/server/plugin.test.ts index 77cc2ac9ca510..ca932e92d98bd 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.test.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.test.ts @@ -135,6 +135,45 @@ describe('Telemetry Collection Manager', () => { collectionStrategy.clusterDetailsGetter.mock.calls[0][0].soClient ).toBeInstanceOf(TelemetrySavedObjectsClient); }); + + it('calls getStats with passed refreshCache config', async () => { + const getStatsCollectionConfig: jest.SpyInstance< + TelemetryCollectionManagerPlugin['getStatsCollectionConfig'] + // @ts-expect-error spying on private method. + > = jest.spyOn(telemetryCollectionManager, 'getStatsCollectionConfig'); + await setupApi.getStats(config); + await setupApi.getStats({ ...config, refreshCache: false }); + await setupApi.getStats({ ...config, refreshCache: true }); + + expect(getStatsCollectionConfig).toBeCalledTimes(3); + expect(getStatsCollectionConfig).toHaveBeenNthCalledWith(1, config, usageCollection); + expect(getStatsCollectionConfig).toHaveNthReturnedWith( + 1, + expect.objectContaining({ refreshCache: false }) + ); + + expect(getStatsCollectionConfig).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ refreshCache: false }), + usageCollection + ); + expect(getStatsCollectionConfig).toHaveNthReturnedWith( + 2, + expect.objectContaining({ refreshCache: false }) + ); + + expect(getStatsCollectionConfig).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ refreshCache: true }), + usageCollection + ); + expect(getStatsCollectionConfig).toHaveNthReturnedWith( + 3, + expect.objectContaining({ refreshCache: true }) + ); + + getStatsCollectionConfig.mockRestore(); + }); }); describe('getOptInStats', () => { @@ -178,9 +217,10 @@ describe('Telemetry Collection Manager', () => { }); }); describe('unencrypted: true', () => { + const mockRequest = httpServerMock.createKibanaRequest(); const config: StatsGetterConfig = { unencrypted: true, - request: httpServerMock.createKibanaRequest(), + request: mockRequest, }; describe('getStats', () => { @@ -212,23 +252,26 @@ describe('Telemetry Collection Manager', () => { ).not.toBeInstanceOf(TelemetrySavedObjectsClient); }); - test('returns cached object on multiple calls', async () => { - collectionStrategy.clusterDetailsGetter.mockResolvedValue([ - { clusterUuid: 'clusterUuid' }, - ]); - collectionStrategy.statsGetter.mockResolvedValue([basicStats]); + it('calls getStats with config { refreshCache: true } even if set to false', async () => { + const getStatsCollectionConfig: jest.SpyInstance< + TelemetryCollectionManagerPlugin['getStatsCollectionConfig'] + // @ts-expect-error spying on private method. + > = jest.spyOn(telemetryCollectionManager, 'getStatsCollectionConfig'); await setupApi.getStats(config); - await expect(setupApi.getStats(config)).resolves.toStrictEqual([ - { - clusterUuid: 'clusterUuid', - stats: { - ...basicStats, - cacheDetails: { updatedAt: expect.any(String), fetchedAt: expect.any(String) }, - collectionSource: 'test_collection', - }, - }, - ]); + expect(getStatsCollectionConfig).toBeCalledTimes(1); + expect(getStatsCollectionConfig).toBeCalledWith( + expect.not.objectContaining({ refreshCache: true }), + usageCollection + ); + expect(getStatsCollectionConfig).toReturnWith( + expect.objectContaining({ + refreshCache: true, + kibanaRequest: mockRequest, + }) + ); + + getStatsCollectionConfig.mockRestore(); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index bdce2c8be31d8..fad51ca1dbfde 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -127,7 +127,7 @@ export class TelemetryCollectionManagerPlugin const soClient = this.getSavedObjectsClient(config); // Provide the kibanaRequest so opted-in plugins can scope their custom clients only if the request is not encrypted const kibanaRequest = config.unencrypted ? config.request : void 0; - const refreshCache = !!config.refreshCache; + const refreshCache = config.unencrypted ? true : !!config.refreshCache; if (esClient && soClient) { return { usageCollection, esClient, soClient, kibanaRequest, refreshCache }; diff --git a/x-pack/test/api_integration/apis/telemetry/telemetry.ts b/x-pack/test/api_integration/apis/telemetry/telemetry.ts index fb0d56f7d7532..088678a74813b 100644 --- a/x-pack/test/api_integration/apis/telemetry/telemetry.ts +++ b/x-pack/test/api_integration/apis/telemetry/telemetry.ts @@ -185,40 +185,37 @@ export default function ({ getService }: FtrProviderContext) { const archive = 'x-pack/test/functional/es_archives/monitoring/basic_6.3.x'; const fromTimestamp = '2018-07-23T22:54:59.087Z'; const toTimestamp = '2018-07-23T22:55:05.933Z'; - let cacheLastUpdated: string[] = []; before(async () => { await esArchiver.load(archive); await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp); - // hit the endpoint to cache results - const { body }: { body: UnencryptedTelemetryPayload } = await supertest + await supertest .post('/api/telemetry/v2/clusters/_stats') .set('kbn-xsrf', 'xxx') .send({ unencrypted: true, refreshCache: true }) .expect(200); - - cacheLastUpdated = getCacheDetails(body).map(({ updatedAt }) => updatedAt); }); after(() => esArchiver.unload(archive)); + }); - it('returns cached results by default', async () => { - const now = Date.now(); - const { body }: { body: UnencryptedTelemetryPayload } = await supertest - .post('/api/telemetry/v2/clusters/_stats') - .set('kbn-xsrf', 'xxx') - .send({ unencrypted: true }) - .expect(200); + it('returns non-cached results when unencrypted', async () => { + const now = Date.now(); + const { body }: { body: UnencryptedTelemetryPayload } = await supertest + .post('/api/telemetry/v2/clusters/_stats') + .set('kbn-xsrf', 'xxx') + .send({ unencrypted: true }) + .expect(200); - expect(body).length(2); + expect(body).length(1); - const cacheDetails = getCacheDetails(body); - // Check that the fetched payload is actually cached by comparing cache and updatedAt timestamps - expect(cacheDetails.map(({ updatedAt }) => updatedAt)).to.eql(cacheLastUpdated); - // Check that the fetchedAt timestamp is updated when the data is fethed - cacheDetails.forEach(({ fetchedAt }) => { - expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now); - }); + const cacheDetails = getCacheDetails(body); + cacheDetails.forEach(({ fetchedAt, updatedAt }) => { + // Check that the cache is fresh by comparing updatedAt timestamp with + // the timestamp the data was fetched. + expect(new Date(updatedAt).getTime()).to.be.greaterThan(now); + // Check that the fetchedAt timestamp is updated when the data is fetched + expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now); }); }); @@ -235,7 +232,7 @@ export default function ({ getService }: FtrProviderContext) { // Check that the cache is fresh by comparing updatedAt timestamp with // the timestamp the data was fetched. expect(new Date(updatedAt).getTime()).to.be.greaterThan(now); - // Check that the fetchedAt timestamp is updated when the data is fethed + // Check that the fetchedAt timestamp is updated when the data is fetched expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now); }); });