[ResponseOps][MaintenanceWindow] Introduce pagination for MW find API (#197172)

Fixes: https://github.com/elastic/kibana/issues/193076

This PR introduce pagination for our MW find API.

How to test:

Use postman/insomnia/curl.
Do not forget to add this header: `x-elastic-internal-origin: Kibana`,
because this endpoint in internal.

Basically you need to do something like this:
```
GET http://localhost:5601/top/internal/alerting/rules/maintenance_window/_find?page=3&per_page=3
```

Try different page and per_page combination. Try without them.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia 2024-10-30 13:54:58 +01:00 committed by GitHub
parent fd615c72e6
commit 6645e74707
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 455 additions and 33 deletions

View file

@ -5,6 +5,20 @@
* 2.0.
*/
export type { FindMaintenanceWindowsResponse } from './types/latest';
export {
findMaintenanceWindowsRequestQuerySchema,
findMaintenanceWindowsResponseBodySchema,
} from './schemas/latest';
export type {
FindMaintenanceWindowsRequestQuery,
FindMaintenanceWindowsResponse,
} from './types/latest';
export type { FindMaintenanceWindowsResponse as FindMaintenanceWindowsResponseV1 } from './types/v1';
export {
findMaintenanceWindowsRequestQuerySchema as findMaintenanceWindowsRequestQuerySchemaV1,
findMaintenanceWindowsResponseBodySchema as findMaintenanceWindowsResponseBodySchemaV1,
} from './schemas/v1';
export type {
FindMaintenanceWindowsRequestQuery as FindMaintenanceWindowsRequestQueryV1,
FindMaintenanceWindowsResponse as FindMaintenanceWindowsResponseV1,
} from './types/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,53 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { maintenanceWindowResponseSchemaV1 } from '../../../response';
const MAX_DOCS = 10000;
export const findMaintenanceWindowsRequestQuerySchema = schema.object(
{
page: schema.maybe(
schema.number({
defaultValue: 1,
min: 1,
max: MAX_DOCS,
meta: {
description: 'The page number to return.',
},
})
),
per_page: schema.maybe(
schema.number({
defaultValue: 20,
min: 0,
max: 100,
meta: {
description: 'The number of maintenance windows to return per page.',
},
})
),
},
{
validate: (params) => {
const pageAsNumber = params.page ?? 0;
const perPageAsNumber = params.per_page ?? 0;
if (Math.max(pageAsNumber, pageAsNumber * perPageAsNumber) > MAX_DOCS) {
return `The number of documents is too high. Paginating through more than ${MAX_DOCS} documents is not possible.`;
}
},
}
);
export const findMaintenanceWindowsResponseBodySchema = schema.object({
page: schema.number(),
per_page: schema.number(),
total: schema.number(),
data: schema.arrayOf(maintenanceWindowResponseSchemaV1),
});

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export type { FindMaintenanceWindowsResponse } from './v1';
export * from './v1';

View file

@ -5,11 +5,15 @@
* 2.0.
*/
import { MaintenanceWindowResponseV1 } from '../../../response';
import { TypeOf } from '@kbn/config-schema';
import {
findMaintenanceWindowsResponseBodySchema,
findMaintenanceWindowsRequestQuerySchema,
} from '..';
export interface FindMaintenanceWindowsResponse {
body: {
data: MaintenanceWindowResponseV1[];
total: number;
};
}
export type FindMaintenanceWindowsResponse = TypeOf<
typeof findMaintenanceWindowsResponseBodySchema
>;
export type FindMaintenanceWindowsRequestQuery = TypeOf<
typeof findMaintenanceWindowsRequestQuerySchema
>;

View file

@ -6,7 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { maintenanceWindowStatusV1 } from '..';
import { maintenanceWindowStatus as maintenanceWindowStatusV1 } from '../constants/v1';
import { maintenanceWindowCategoryIdsSchemaV1 } from '../../shared';
import { rRuleResponseSchemaV1 } from '../../../r_rule';
import { alertsFilterQuerySchemaV1 } from '../../../alerts_filter_query';

View file

@ -16,7 +16,7 @@ export async function findMaintenanceWindows({
}: {
http: HttpSetup;
}): Promise<MaintenanceWindow[]> {
const res = await http.get<FindMaintenanceWindowsResponse['body']>(
const res = await http.get<FindMaintenanceWindowsResponse>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_find`
);
return res.data.map((mw) => transformMaintenanceWindowResponse(mw));

View file

@ -17,6 +17,7 @@ import {
MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
} from '../../../../../common';
import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/test_helpers';
import { findMaintenanceWindowsParamsSchema } from './schemas';
const savedObjectsClient = savedObjectsClientMock.create();
const uiSettings = uiSettingsServiceMock.createClient();
@ -37,8 +38,47 @@ describe('MaintenanceWindowClient - find', () => {
jest.useRealTimers();
});
it('throws an error if page is string', async () => {
savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: [
{
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
id: 'test-1',
},
{
attributes: getMockMaintenanceWindow({ expirationDate: new Date().toISOString() }),
id: 'test-2',
},
],
page: 1,
per_page: 5,
} as unknown as SavedObjectsFindResponse);
await expect(
// @ts-expect-error: testing validation of strings
findMaintenanceWindows(mockContext, { page: 'dfsd', perPage: 10 })
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Error validating find maintenance windows data - [page]: expected value of type [number] but got [string]"'
);
});
it('throws an error if savedObjectsClient.find will throw an error', async () => {
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
savedObjectsClient.find.mockImplementation(() => {
throw new Error('something went wrong!');
});
await expect(
findMaintenanceWindows(mockContext, { page: 1, perPage: 10 })
).rejects.toThrowErrorMatchingInlineSnapshot(
'"Failed to find maintenance window, Error: Error: something went wrong!: something went wrong!"'
);
});
it('should find maintenance windows', async () => {
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
const spy = jest.spyOn(findMaintenanceWindowsParamsSchema, 'validate');
savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: [
@ -51,10 +91,13 @@ describe('MaintenanceWindowClient - find', () => {
id: 'test-2',
},
],
page: 1,
per_page: 5,
} as unknown as SavedObjectsFindResponse);
const result = await findMaintenanceWindows(mockContext);
const result = await findMaintenanceWindows(mockContext, {});
expect(spy).toHaveBeenCalledWith({});
expect(savedObjectsClient.find).toHaveBeenLastCalledWith({
type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
});
@ -62,5 +105,7 @@ describe('MaintenanceWindowClient - find', () => {
expect(result.data.length).toEqual(2);
expect(result.data[0].id).toEqual('test-1');
expect(result.data[1].id).toEqual('test-2');
expect(result.page).toEqual(1);
expect(result.perPage).toEqual(5);
});
});

View file

@ -9,17 +9,35 @@ import Boom from '@hapi/boom';
import { MaintenanceWindowClientContext } from '../../../../../common';
import { transformMaintenanceWindowAttributesToMaintenanceWindow } from '../../transforms';
import { findMaintenanceWindowSo } from '../../../../data/maintenance_window';
import type { FindMaintenanceWindowsResult } from './types';
import type { FindMaintenanceWindowsResult, FindMaintenanceWindowsParams } from './types';
import { findMaintenanceWindowsParamsSchema } from './schemas';
export async function findMaintenanceWindows(
context: MaintenanceWindowClientContext
context: MaintenanceWindowClientContext,
params?: FindMaintenanceWindowsParams
): Promise<FindMaintenanceWindowsResult> {
const { savedObjectsClient, logger } = context;
try {
const result = await findMaintenanceWindowSo({ savedObjectsClient });
if (params) {
findMaintenanceWindowsParamsSchema.validate(params);
}
} catch (error) {
throw Boom.badRequest(`Error validating find maintenance windows data - ${error.message}`);
}
try {
const result = await findMaintenanceWindowSo({
savedObjectsClient,
...(params
? { savedObjectsFindOptions: { page: params.page, perPage: params.perPage } }
: {}),
});
return {
page: result.page,
perPage: result.per_page,
total: result.total,
data: result.saved_objects.map((so) =>
transformMaintenanceWindowAttributesToMaintenanceWindow({
attributes: so.attributes,

View file

@ -0,0 +1,13 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const findMaintenanceWindowsParamsSchema = schema.object({
perPage: schema.maybe(schema.number()),
page: schema.maybe(schema.number()),
});

View file

@ -9,5 +9,8 @@ import { schema } from '@kbn/config-schema';
import { maintenanceWindowSchema } from '../../../schemas';
export const findMaintenanceWindowsResultSchema = schema.object({
page: schema.number(),
perPage: schema.number(),
data: schema.arrayOf(maintenanceWindowSchema),
total: schema.number(),
});

View file

@ -6,3 +6,4 @@
*/
export { findMaintenanceWindowsResultSchema } from './find_maintenance_windows_result_schema';
export { findMaintenanceWindowsParamsSchema } from './find_maintenance_window_params_schema';

View file

@ -0,0 +1,11 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { findMaintenanceWindowsParamsSchema } from '../schemas';
export type FindMaintenanceWindowsParams = TypeOf<typeof findMaintenanceWindowsParamsSchema>;

View file

@ -6,3 +6,4 @@
*/
export type { FindMaintenanceWindowsResult } from './find_maintenance_window_result';
export type { FindMaintenanceWindowsParams } from './find_maintenance_window_params';

View file

@ -24,7 +24,7 @@ export const findMaintenanceWindowSo = <MaintenanceWindowAggregation = Record<st
const { savedObjectsClient, savedObjectsFindOptions } = params;
return savedObjectsClient.find<MaintenanceWindowAttributes, MaintenanceWindowAggregation>({
...savedObjectsFindOptions,
...(savedObjectsFindOptions ? savedObjectsFindOptions : {}),
type: MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE,
});
};

View file

@ -13,7 +13,10 @@ import type { GetMaintenanceWindowParams } from '../application/maintenance_wind
import { updateMaintenanceWindow } from '../application/maintenance_window/methods/update/update_maintenance_window';
import type { UpdateMaintenanceWindowParams } from '../application/maintenance_window/methods/update/types';
import { findMaintenanceWindows } from '../application/maintenance_window/methods/find/find_maintenance_windows';
import type { FindMaintenanceWindowsResult } from '../application/maintenance_window/methods/find/types';
import type {
FindMaintenanceWindowsResult,
FindMaintenanceWindowsParams,
} from '../application/maintenance_window/methods/find/types';
import { deleteMaintenanceWindow } from '../application/maintenance_window/methods/delete/delete_maintenance_window';
import type { DeleteMaintenanceWindowParams } from '../application/maintenance_window/methods/delete/types';
import { archiveMaintenanceWindow } from '../application/maintenance_window/methods/archive/archive_maintenance_window';
@ -75,7 +78,8 @@ export class MaintenanceWindowClient {
getMaintenanceWindow(this.context, params);
public update = (params: UpdateMaintenanceWindowParams): Promise<MaintenanceWindow> =>
updateMaintenanceWindow(this.context, params);
public find = (): Promise<FindMaintenanceWindowsResult> => findMaintenanceWindows(this.context);
public find = (params?: FindMaintenanceWindowsParams): Promise<FindMaintenanceWindowsResult> =>
findMaintenanceWindows(this.context, params);
public delete = (params: DeleteMaintenanceWindowParams): Promise<{}> =>
deleteMaintenanceWindow(this.context, params);
public archive = (params: ArchiveMaintenanceWindowParams): Promise<MaintenanceWindow> =>

View file

@ -22,6 +22,9 @@ jest.mock('../../../../lib/license_api_access', () => ({
}));
const mockMaintenanceWindows = {
page: 1,
perPage: 3,
total: 2,
data: [
{
...getMockMaintenanceWindow(),
@ -67,11 +70,54 @@ describe('findMaintenanceWindowsRoute', () => {
await handler(context, req, res);
expect(maintenanceWindowClient.find).toHaveBeenCalled();
expect(maintenanceWindowClient.find).toHaveBeenCalledWith({});
expect(res.ok).toHaveBeenLastCalledWith({
body: {
data: mockMaintenanceWindows.data.map((data) => rewriteMaintenanceWindowRes(data)),
total: 2,
page: 1,
per_page: 3,
},
});
});
test('should find the maintenance windows with query', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
findMaintenanceWindowsRoute(router, licenseState);
maintenanceWindowClient.find.mockResolvedValueOnce(mockMaintenanceWindows);
const [config, handler] = router.get.mock.calls[0];
const [context, req, res] = mockHandlerArguments(
{ maintenanceWindowClient },
{
query: {
page: 1,
per_page: 3,
},
}
);
expect(config.path).toEqual('/internal/alerting/rules/maintenance_window/_find');
expect(config.options).toMatchInlineSnapshot(`
Object {
"access": "internal",
"tags": Array [
"access:read-maintenance-window",
],
}
`);
await handler(context, req, res);
expect(maintenanceWindowClient.find).toHaveBeenCalledWith({ page: 1, perPage: 3 });
expect(res.ok).toHaveBeenLastCalledWith({
body: {
data: mockMaintenanceWindows.data.map((data) => rewriteMaintenanceWindowRes(data)),
total: 2,
page: 1,
per_page: 3,
},
});
});

View file

@ -15,7 +15,15 @@ import {
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../../../common';
import type { FindMaintenanceWindowsResult } from '../../../../application/maintenance_window/methods/find/types';
import type { FindMaintenanceWindowsResponseV1 } from '../../../../../common/routes/maintenance_window/apis/find';
import { transformMaintenanceWindowToResponseV1 } from '../../transforms';
import {
findMaintenanceWindowsRequestQuerySchemaV1,
findMaintenanceWindowsResponseBodySchemaV1,
type FindMaintenanceWindowsRequestQueryV1,
} from '../../../../../common/routes/maintenance_window/apis/find';
import {
transformFindMaintenanceWindowParamsV1,
transformFindMaintenanceWindowResponseV1,
} from './transforms';
export const findMaintenanceWindowsRoute = (
router: IRouter<AlertingRequestHandlerContext>,
@ -24,7 +32,23 @@ export const findMaintenanceWindowsRoute = (
router.get(
{
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/_find`,
validate: {},
validate: {
request: {
query: findMaintenanceWindowsRequestQuerySchemaV1,
},
response: {
200: {
body: () => findMaintenanceWindowsResponseBodySchemaV1,
description: 'Indicates a successful call.',
},
400: {
description: 'Indicates an invalid schema or parameters.',
},
403: {
description: 'Indicates that this call is forbidden.',
},
},
},
options: {
access: 'internal',
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`],
@ -34,20 +58,17 @@ export const findMaintenanceWindowsRoute = (
verifyAccessAndContext(licenseState, async function (context, req, res) {
licenseState.ensureLicenseForMaintenanceWindow();
const query: FindMaintenanceWindowsRequestQueryV1 = req.query || {};
const maintenanceWindowClient = (await context.alerting).getMaintenanceWindowClient();
const result: FindMaintenanceWindowsResult = await maintenanceWindowClient.find();
const options = transformFindMaintenanceWindowParamsV1(query);
const findResult: FindMaintenanceWindowsResult = await maintenanceWindowClient.find(
options
);
const responseBody: FindMaintenanceWindowsResponseV1 =
transformFindMaintenanceWindowResponseV1(findResult);
const response: FindMaintenanceWindowsResponseV1 = {
body: {
data: result.data.map((maintenanceWindow) =>
transformMaintenanceWindowToResponseV1(maintenanceWindow)
),
total: result.data.length,
},
};
return res.ok(response);
return res.ok({ body: responseBody });
})
)
);

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export { transformFindMaintenanceWindowParams } from './transform_find_maintenance_window_params/latest';
export { transformFindMaintenanceWindowResponse } from './transform_find_maintenance_window_to_response/latest';
export { transformFindMaintenanceWindowParams as transformFindMaintenanceWindowParamsV1 } from './transform_find_maintenance_window_params/v1';
export { transformFindMaintenanceWindowResponse as transformFindMaintenanceWindowResponseV1 } from './transform_find_maintenance_window_to_response/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,16 @@
/*
* 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 { FindMaintenanceWindowsRequestQuery } from '../../../../../../../common/routes/maintenance_window/apis/find';
import { FindMaintenanceWindowsParams } from '../../../../../../application/maintenance_window/methods/find/types';
export const transformFindMaintenanceWindowParams = (
params: FindMaintenanceWindowsRequestQuery
): FindMaintenanceWindowsParams => ({
...(params.page ? { page: params.page } : {}),
...(params.per_page ? { perPage: params.per_page } : {}),
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,24 @@
/*
* 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 { transformMaintenanceWindowToResponseV1 } from '../../../../transforms';
import type { FindMaintenanceWindowsResponseV1 } from '../../../../../../../common/routes/maintenance_window/apis/find';
import type { MaintenanceWindow } from '../../../../../../application/maintenance_window/types';
import type { FindMaintenanceWindowsResult } from '../../../../../../application/maintenance_window/methods/find/types';
export const transformFindMaintenanceWindowResponse = (
result: FindMaintenanceWindowsResult
): FindMaintenanceWindowsResponseV1 => {
return {
page: result.page,
per_page: result.perPage,
total: result.total,
data: result.data.map((maintenanceWindow: MaintenanceWindow) =>
transformMaintenanceWindowToResponseV1(maintenanceWindow)
),
};
};

View file

@ -90,6 +90,118 @@ export default function findMaintenanceWindowTests({ getService }: FtrProviderCo
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('should handle find maintenance window request with pagination', async () => {
const { body: createdMaintenanceWindow1 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send(createParams);
const { body: createdMaintenanceWindow2 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send({ ...createParams, title: 'test-maintenance-window2' });
objectRemover.add(
space.id,
createdMaintenanceWindow1.id,
'rules/maintenance_window',
'alerting',
true
);
objectRemover.add(
space.id,
createdMaintenanceWindow2.id,
'rules/maintenance_window',
'alerting',
true
);
const response = await supertestWithoutAuth
.get(
`${getUrlPrefix(
space.id
)}/internal/alerting/rules/maintenance_window/_find?page=1&per_page=1`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all_with_restricted_fixture at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: 'Forbidden',
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.body.total).to.eql(2);
expect(response.statusCode).to.eql(200);
expect(response.body.data[0].id).to.eql(createdMaintenanceWindow1.id);
expect(response.body.data[0].title).to.eql('test-maintenance-window');
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it('throw an error for find maintenance window request with pagination if docs count more 10k', async () => {
const { body: createdMaintenanceWindow1 } = await supertest
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
.set('kbn-xsrf', 'foo')
.send(createParams);
objectRemover.add(
space.id,
createdMaintenanceWindow1.id,
'rules/maintenance_window',
'alerting',
true
);
const response = await supertestWithoutAuth
.get(
`${getUrlPrefix(
space.id
)}/internal/alerting/rules/maintenance_window/_find?page=101&per_page=100`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.send({});
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'space_1_all_with_restricted_fixture at space1':
case 'space_1_all_alerts_none_actions at space1':
expect(response.statusCode).to.eql(403);
expect(response.body).to.eql({
error: 'Forbidden',
message: 'Forbidden',
statusCode: 403,
});
break;
case 'global_read at space1':
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.body).to.eql({
statusCode: 400,
error: 'Bad Request',
message:
'[request query]: The number of documents is too high. Paginating through more than 10000 documents is not possible.',
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});