Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/dev/i18n/extract_default_translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function matchEntriesWithExctractors(inputPath, options = {}) {
'**/dist/**',
'**/target/**',
'**/vendor/**',
'**/build/**',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.d.ts',
].concat(additionalIgnore);
Expand Down
1 change: 0 additions & 1 deletion src/dev/i18n/tasks/extract_untracked_translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/**',
Expand Down
103 changes: 103 additions & 0 deletions src/plugins/telemetry/server/routes/telemetry_usage_stats.test.ts
Original file line number Diff line number Diff line change
@@ -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<RequestHandlerContext>,
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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function registerTelemetryUsageStatsRoutes(
const statsConfig: StatsGetterConfig = {
request: req,
unencrypted,
refreshCache,
refreshCache: unencrypted || refreshCache,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Should this be handled down inside the telemetryCollectionManager.getStats? It may cause different behaviours if called from somewhere else. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to keep the logic at the route level since we might want to set up things differently in different places

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see where you're coming from. However, the cache is implemented in telemetryCollectionManager.getStats, I don't see why others may want to use it in a different way. Especially bearing in mind that, getStats is the one deciding how to scope the requests (so any other place using unencrypted: true, refreshCache: false might expose again values retrieved with different permissions to the caller.

IMO, the scoping and the refresh algos should be together.

};

const stats = await telemetryCollectionManager.getStats(statsConfig);
Expand Down
39 changes: 39 additions & 0 deletions src/plugins/telemetry_collection_manager/server/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧡

* 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<TelemetryCollectionManagerPluginSetup>;
export type Start = jest.Mocked<TelemetryCollectionManagerPluginStart>;

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;
}
75 changes: 59 additions & 16 deletions src/plugins/telemetry_collection_manager/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/plugins/telemetry_collection_manager/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
39 changes: 18 additions & 21 deletions x-pack/test/api_integration/apis/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand All @@ -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);
});
});
Expand Down