[Uptime] Settings public API (#163400)

This commit is contained in:
Shahzad 2023-10-23 15:14:37 +02:00 committed by GitHub
parent 8805e74f5d
commit 882e0bf81a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 365 additions and 185 deletions

View file

@ -0,0 +1,11 @@
[[uptime-apis]]
== Uptime APIs
The following APIs are available for Uptime.
* <<get-settings-api, Get settings API>> to get a settings
* <<update-settings-api, Update settings API>> to update the attributes for existing settings
include::uptime/get-settings.asciidoc[leveloffset=+1]
include::uptime/update-settings.asciidoc[leveloffset=+1]

View file

@ -0,0 +1,39 @@
[[get-settings-api]]
== Get settings API
++++
<titleabbrev>Get settings</titleabbrev>
++++
Retrieve uptime settings existing settings.
[[get-settings-api-request]]
=== {api-request-title}
`GET <kibana host>:<port>/api/uptime/settings`
`GET <kibana host>:<port>/s/<space_id>/api/uptime/settings`
=== {api-prereq-title}
You must have `read` privileges for the *uptime* feature in *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
The API returns the following:
[source,sh]
--------------------------------------------------
{
"heartbeatIndices": "heartbeat-8*",
"certExpirationThreshold": 30,
"certAgeThreshold": 730,
"defaultConnectors": [
"08990f40-09c5-11ee-97ae-912b222b13d4",
"db25f830-2318-11ee-9391-6b0c030836d6"
],
"defaultEmail": {
"to": [],
"cc": [],
"bcc": []
}
}
--------------------------------------------------

View file

@ -0,0 +1,117 @@
[[update-settings-api]]
== Update settings API
++++
<titleabbrev>Update settings</titleabbrev>
++++
Updates uptime settings attributes like heartbeatIndices, certExpirationThreshold, certAgeThreshold, defaultConnectors or defaultEmail
=== {api-request-title}
`PUT <kibana host>:<port>/api/uptime/settings`
`PUT <kibana host>:<port>/s/<space_id>/api/uptime/settings`
=== {api-prereq-title}
You must have `all` privileges for the *uptime* feature in *{observability}* section of the
<<kibana-feature-privileges,{kib} feature privileges>>.
[[settings-api-update-path-params]]
==== Path parameters
`space_id`::
(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used.
[[api-update-request-body]]
==== Request body
A partial update is supported, provided settings keys will be merged with existing settings.
`heartbeatIndices`::
(Optional, string) index pattern string to be used within uptime app/alerts to query heartbeat data. Defaults to `heartbeat-*`.
`certExpirationThreshold`::
(Optional, number) Number of days before a certificate expires to trigger an alert. Defaults to `30`.
`certAgeThreshold`::
(Optional, number) Number of days after a certificate is created to trigger an alert. Defaults to `730`.
`defaultConnectors`::
(Optional, array) List of connector IDs to be used as default connectors for new alerts. Defaults to `[]`.
`defaultEmail`::
(Optional, object) Default email configuration for new alerts. Defaults to `{"to": [], "cc": [], "bcc": []}`.
[[settings-api-update-example]]
==== Example
[source,sh]
--------------------------------------------------
PUT api/uptime/settings
{
"heartbeatIndices": "heartbeat-8*",
"certExpirationThreshold": 30,
"certAgeThreshold": 730,
"defaultConnectors": [
"08990f40-09c5-11ee-97ae-912b222b13d4",
"db25f830-2318-11ee-9391-6b0c030836d6"
],
"defaultEmail": {
"to": [],
"cc": [],
"bcc": []
}
}
--------------------------------------------------
The API returns the following:
[source,json]
--------------------------------------------------
{
"heartbeatIndices": "heartbeat-8*",
"certExpirationThreshold": 30,
"certAgeThreshold": 730,
"defaultConnectors": [
"08990f40-09c5-11ee-97ae-912b222b13d4",
"db25f830-2318-11ee-9391-6b0c030836d6"
],
"defaultEmail": {
"to": [],
"cc": [],
"bcc": []
}
}
--------------------------------------------------
[[settings-api-partial-update-example]]
==== Partial update example
[source,sh]
--------------------------------------------------
PUT api/uptime/settings
{
"heartbeatIndices": "heartbeat-8*",
}
--------------------------------------------------
The API returns the following:
[source,json]
--------------------------------------------------
{
"heartbeatIndices": "heartbeat-8*",
"certExpirationThreshold": 30,
"certAgeThreshold": 730,
"defaultConnectors": [
"08990f40-09c5-11ee-97ae-912b222b13d4",
"db25f830-2318-11ee-9391-6b0c030836d6"
],
"defaultEmail": {
"to": [],
"cc": [],
"bcc": []
}
}
--------------------------------------------------

View file

@ -109,3 +109,4 @@ include::{kib-repo-dir}/api/osquery-manager.asciidoc[]
include::{kib-repo-dir}/api/short-urls.asciidoc[]
include::{kib-repo-dir}/api/task-manager/health.asciidoc[]
include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[]
include::{kib-repo-dir}/api/uptime-api.asciidoc[]

View file

@ -48,5 +48,5 @@ export enum SYNTHETICS_API_URLS {
SYNTHETICS_MONITORS_PROJECT_UPDATE = '/api/synthetics/project/{projectName}/monitors/_bulk_update',
SYNTHETICS_MONITORS_PROJECT_DELETE = '/api/synthetics/project/{projectName}/monitors/_bulk_delete',
DYNAMIC_SETTINGS = `/internal/uptime/dynamic_settings`,
DYNAMIC_SETTINGS = `/api/uptime/settings`,
}

View file

@ -24,7 +24,6 @@ journey('OverviewScrolling', async ({ page, params }) => {
const listOfRequests: string[] = [];
const expected = [
'http://localhost:5620/internal/synthetics/service/enablement',
'http://localhost:5620/internal/uptime/dynamic_settings',
'http://localhost:5620/internal/synthetics/monitor/filters',
'http://localhost:5620/internal/uptime/service/locations',
'http://localhost:5620/internal/synthetics/overview?sortField=status&sortOrder=asc&',

View file

@ -21,20 +21,29 @@ import {
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { LocationMonitor } from '.';
const apiPath = SYNTHETICS_API_URLS.DYNAMIC_SETTINGS;
interface SaveApiRequest {
settings: DynamicSettings;
}
export const getDynamicSettings = async (): Promise<DynamicSettings> => {
return await apiService.get(apiPath, undefined, DynamicSettingsCodec);
return await apiService.get(
SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
{ version: '2023-10-31' },
DynamicSettingsCodec
);
};
export const setDynamicSettings = async ({
settings,
}: SaveApiRequest): Promise<DynamicSettingsSaveResponse> => {
return await apiService.post(apiPath, settings, DynamicSettingsSaveCodec);
return await apiService.put(
SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
settings,
DynamicSettingsSaveCodec,
{
version: '2023-10-31',
}
);
};
export const fetchLocationMonitors = async (): Promise<LocationMonitor[]> => {

View file

@ -9,7 +9,7 @@ import { isRight } from 'fp-ts/lib/Either';
import { formatErrors } from '@kbn/securitysolution-io-ts-utils';
import { HttpFetchQuery, HttpSetup } from '@kbn/core/public';
import { FETCH_STATUS, AddInspectorRequest } from '@kbn/observability-shared-plugin/public';
type Params = HttpFetchQuery & { version?: string };
class ApiService {
private static instance: ApiService;
private _http!: HttpSetup;
@ -59,16 +59,13 @@ class ApiService {
return response;
}
public async get<T>(
apiUrl: string,
params?: HttpFetchQuery,
decodeType?: any,
asResponse = false
) {
public async get<T>(apiUrl: string, params: Params = {}, decodeType?: any, asResponse = false) {
const { version, ...queryParams } = params;
const response = await this._http!.fetch<T>({
path: apiUrl,
query: params,
query: queryParams,
asResponse,
version,
});
this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false });
@ -76,11 +73,14 @@ class ApiService {
return this.parseResponse(response, apiUrl, decodeType);
}
public async post<T>(apiUrl: string, data?: any, decodeType?: any, params?: HttpFetchQuery) {
public async post<T>(apiUrl: string, data?: any, decodeType?: any, params: Params = {}) {
const { version, ...queryParams } = params;
const response = await this._http!.post<T>(apiUrl, {
method: 'POST',
body: JSON.stringify(data),
query: params,
query: queryParams,
version,
});
this.addInspectorRequest?.({ data: response, status: FETCH_STATUS.SUCCESS, loading: false });
@ -88,10 +88,14 @@ class ApiService {
return this.parseResponse(response, apiUrl, decodeType);
}
public async put<T>(apiUrl: string, data?: any, decodeType?: any) {
public async put<T>(apiUrl: string, data?: any, decodeType?: any, params: Params = {}) {
const { version, ...queryParams } = params;
const response = await this._http!.put<T>(apiUrl, {
method: 'PUT',
body: JSON.stringify(data),
query: queryParams,
version,
});
return this.parseResponse(response, apiUrl, decodeType);

View file

@ -6,7 +6,7 @@
*/
export enum API_URLS {
DYNAMIC_SETTINGS = `/internal/uptime/dynamic_settings`,
DYNAMIC_SETTINGS = `/api/uptime/settings`,
INDEX_STATUS = '/internal/uptime/index_status',
MONITOR_LIST = `/internal/uptime/monitor/list`,
MONITOR_LOCATIONS = `/internal/uptime/monitor/locations`,

View file

@ -109,3 +109,5 @@ export const SYNTHETICS_INDEX_PATTERN = 'synthetics-*';
export const LICENSE_NOT_ACTIVE_ERROR = 'License not active';
export const LICENSE_MISSING_ERROR = 'Missing license information';
export const LICENSE_NOT_SUPPORTED_ERROR = 'License not supported';
export const INITIAL_REST_VERSION = '2023-10-31';

View file

@ -12,20 +12,24 @@ import {
DynamicSettingsSaveCodec,
} from '../../../../common/runtime_types';
import { apiService } from './utils';
import { API_URLS } from '../../../../common/constants';
const apiPath = API_URLS.DYNAMIC_SETTINGS;
import { API_URLS, INITIAL_REST_VERSION } from '../../../../common/constants';
interface SaveApiRequest {
settings: DynamicSettings;
}
export const getDynamicSettings = async (): Promise<DynamicSettings> => {
return await apiService.get(apiPath, undefined, DynamicSettingsCodec);
return await apiService.get(
API_URLS.DYNAMIC_SETTINGS,
{ version: INITIAL_REST_VERSION },
DynamicSettingsCodec
);
};
export const setDynamicSettings = async ({
settings,
}: SaveApiRequest): Promise<DynamicSettingsSaveResponse> => {
return await apiService.post(apiPath, settings, DynamicSettingsSaveCodec);
return await apiService.put(API_URLS.DYNAMIC_SETTINGS, settings, DynamicSettingsSaveCodec, {
version: INITIAL_REST_VERSION,
});
};

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import { SavedObjectsErrorHelpers, SavedObjectsServiceSetup } from '@kbn/core/server';
import {
SavedObjectsClientContract,
SavedObjectsErrorHelpers,
SavedObjectsServiceSetup,
} from '@kbn/core/server';
import { DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES } from '../../../constants/settings';
import { DynamicSettingsAttributes } from '../../../runtime_types/settings';
@ -20,7 +24,10 @@ export const registerUptimeSavedObjects = (savedObjectsService: SavedObjectsServ
export interface UMSavedObjectsAdapter {
config: UptimeConfig | null;
getUptimeDynamicSettings: UMSavedObjectsQueryFn<DynamicSettingsAttributes>;
setUptimeDynamicSettings: UMSavedObjectsQueryFn<void, DynamicSettingsAttributes>;
setUptimeDynamicSettings: (
client: SavedObjectsClientContract,
attr: DynamicSettingsAttributes
) => Promise<DynamicSettingsAttributes>;
}
export const savedObjectsAdapter: UMSavedObjectsAdapter = {
@ -43,10 +50,15 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = {
throw getErr;
}
},
setUptimeDynamicSettings: async (client, settings: DynamicSettingsAttributes | undefined) => {
await client.create(umDynamicSettings.name, settings, {
id: settingsObjectId,
overwrite: true,
});
setUptimeDynamicSettings: async (client, settings: DynamicSettingsAttributes) => {
const newObj = await client.create<DynamicSettingsAttributes>(
umDynamicSettings.name,
settings,
{
id: settingsObjectId,
overwrite: true,
}
);
return newObj.attributes;
},
};

View file

@ -1,83 +0,0 @@
/*
* 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 { validateCertsValues } from './dynamic_settings';
describe('dynamic settings', () => {
describe('validateCertValues', () => {
it(`doesn't allow age threshold values less than 0`, () => {
expect(
validateCertsValues({
certAgeThreshold: -1,
certExpirationThreshold: 2,
heartbeatIndices: 'foo',
defaultConnectors: [],
})
).toMatchInlineSnapshot(`
Object {
"certAgeThreshold": "Value must be greater than 0.",
}
`);
});
it(`doesn't allow non-integer age threshold values`, () => {
expect(
validateCertsValues({
certAgeThreshold: 10.2,
certExpirationThreshold: 2,
heartbeatIndices: 'foo',
defaultConnectors: [],
})
).toMatchInlineSnapshot(`
Object {
"certAgeThreshold": "Value must be an integer.",
}
`);
});
it(`doesn't allow expiration threshold values less than 0`, () => {
expect(
validateCertsValues({
certAgeThreshold: 2,
certExpirationThreshold: -1,
heartbeatIndices: 'foo',
defaultConnectors: [],
})
).toMatchInlineSnapshot(`
Object {
"certExpirationThreshold": "Value must be greater than 0.",
}
`);
});
it(`doesn't allow non-integer expiration threshold values`, () => {
expect(
validateCertsValues({
certAgeThreshold: 2,
certExpirationThreshold: 1.23,
heartbeatIndices: 'foo',
defaultConnectors: [],
})
).toMatchInlineSnapshot(`
Object {
"certExpirationThreshold": "Value must be an integer.",
}
`);
});
it('allows valid values', () => {
expect(
validateCertsValues({
certAgeThreshold: 2,
certExpirationThreshold: 13,
heartbeatIndices: 'foo',
defaultConnectors: [],
})
).toBeUndefined();
});
});
});

View file

@ -6,17 +6,12 @@
*/
import { schema } from '@kbn/config-schema';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { UMServerLibs } from '../lib/lib';
import { DynamicSettings, DynamicSettingsCodec } from '../../../common/runtime_types';
import { DynamicSettings } from '../../../common/runtime_types';
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
import { UMRestApiRouteFactory } from '.';
import { savedObjectsAdapter } from '../lib/saved_objects/saved_objects';
import {
VALUE_MUST_BE_GREATER_THAN_ZERO,
VALUE_MUST_BE_AN_INTEGER,
} from '../../../common/translations';
import { VALUE_MUST_BE_AN_INTEGER } from '../../../common/translations';
import { API_URLS } from '../../../common/constants';
export const createGetDynamicSettingsRoute: UMRestApiRouteFactory<DynamicSettings> = (
@ -28,75 +23,56 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory<DynamicSetting
handler: async ({ savedObjectsClient }) => {
const dynamicSettingsAttributes: DynamicSettingsAttributes =
await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
return {
heartbeatIndices: dynamicSettingsAttributes.heartbeatIndices,
certExpirationThreshold: dynamicSettingsAttributes.certExpirationThreshold,
certAgeThreshold: dynamicSettingsAttributes.certAgeThreshold,
defaultConnectors: dynamicSettingsAttributes.defaultConnectors,
defaultEmail: dynamicSettingsAttributes.defaultEmail,
};
return fromAttribute(dynamicSettingsAttributes);
},
});
export const validateCertsValues = (
settings: DynamicSettings
): Record<string, string> | undefined => {
const errors: any = {};
if (settings.certAgeThreshold <= 0) {
errors.certAgeThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO;
} else if (settings.certAgeThreshold % 1) {
errors.certAgeThreshold = VALUE_MUST_BE_AN_INTEGER;
}
if (settings.certExpirationThreshold <= 0) {
errors.certExpirationThreshold = VALUE_MUST_BE_GREATER_THAN_ZERO;
} else if (settings.certExpirationThreshold % 1) {
errors.certExpirationThreshold = VALUE_MUST_BE_AN_INTEGER;
}
if (errors.certAgeThreshold || errors.certExpirationThreshold) {
return errors;
export const validateInteger = (value: number): string | undefined => {
if (value % 1) {
return VALUE_MUST_BE_AN_INTEGER;
}
};
export const DynamicSettingsSchema = schema.object({
heartbeatIndices: schema.maybe(schema.string({ minLength: 1 })),
certAgeThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })),
certExpirationThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })),
defaultConnectors: schema.maybe(schema.arrayOf(schema.string())),
defaultEmail: schema.maybe(
schema.object({
to: schema.arrayOf(schema.string()),
cc: schema.maybe(schema.arrayOf(schema.string())),
bcc: schema.maybe(schema.arrayOf(schema.string())),
})
),
});
export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (_libs: UMServerLibs) => ({
method: 'POST',
method: 'PUT',
path: API_URLS.DYNAMIC_SETTINGS,
validate: {
body: schema.object({
heartbeatIndices: schema.string(),
certAgeThreshold: schema.number(),
certExpirationThreshold: schema.number(),
defaultConnectors: schema.arrayOf(schema.string()),
defaultEmail: schema.maybe(
schema.object({
to: schema.arrayOf(schema.string()),
cc: schema.maybe(schema.arrayOf(schema.string())),
bcc: schema.maybe(schema.arrayOf(schema.string())),
})
),
}),
body: DynamicSettingsSchema,
},
writeAccess: true,
handler: async ({ savedObjectsClient, request, response }): Promise<any> => {
const decoded = DynamicSettingsCodec.decode(request.body);
const certThresholdErrors = validateCertsValues(request.body as DynamicSettings);
handler: async ({ savedObjectsClient, request }): Promise<DynamicSettingsAttributes> => {
const newSettings = request.body;
const prevSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
if (isRight(decoded) && !certThresholdErrors) {
const newSettings: DynamicSettings = decoded.right;
await savedObjectsAdapter.setUptimeDynamicSettings(
savedObjectsClient,
newSettings as DynamicSettingsAttributes
);
const attr = await savedObjectsAdapter.setUptimeDynamicSettings(savedObjectsClient, {
...prevSettings,
...newSettings,
} as DynamicSettingsAttributes);
return response.ok({
body: {
success: true,
},
});
} else {
const error = PathReporter.report(decoded).join(', ');
return response.badRequest({
body: JSON.stringify(certThresholdErrors) || error,
});
}
return fromAttribute(attr);
},
});
const fromAttribute = (attr: DynamicSettingsAttributes) => {
return {
heartbeatIndices: attr.heartbeatIndices,
certExpirationThreshold: attr.certExpirationThreshold,
certAgeThreshold: attr.certAgeThreshold,
defaultConnectors: attr.defaultConnectors,
defaultEmail: attr.defaultEmail,
};
};

View file

@ -34,8 +34,6 @@ export { uptimeRouteWrapper } from './uptime_route_wrapper';
export const legacyUptimeRestApiRoutes: UMRestApiRouteFactory[] = [
createGetPingsRoute,
createGetIndexStatusRoute,
createGetDynamicSettingsRoute,
createPostDynamicSettingsRoute,
createGetMonitorDetailsRoute,
createGetMonitorLocationsRoute,
createMonitorListRoute,
@ -50,3 +48,8 @@ export const legacyUptimeRestApiRoutes: UMRestApiRouteFactory[] = [
createLastSuccessfulCheckRoute,
createJourneyScreenshotBlocksRoute,
];
export const legacyUptimePublicRestApiRoutes: UMRestApiRouteFactory[] = [
createGetDynamicSettingsRoute,
createPostDynamicSettingsRoute,
];

View file

@ -7,9 +7,16 @@
import { Logger } from '@kbn/core/server';
import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server';
import { INITIAL_REST_VERSION } from '../../common/constants';
import { DynamicSettingsSchema } from './routes/dynamic_settings';
import { UptimeRouter } from '../types';
import { uptimeRequests } from './lib/requests';
import { createRouteWithAuth, legacyUptimeRestApiRoutes, uptimeRouteWrapper } from './routes';
import {
createRouteWithAuth,
legacyUptimePublicRestApiRoutes,
legacyUptimeRestApiRoutes,
uptimeRouteWrapper,
} from './routes';
import { UptimeServerSetup, UptimeCorePluginsSetup } from './lib/adapters';
import { statusCheckAlertFactory } from './lib/alerts/status_check';
@ -62,6 +69,76 @@ export const initUptimeServer = (
}
});
legacyUptimePublicRestApiRoutes.forEach((route) => {
const { method, options, handler, validate, path } = uptimeRouteWrapper(
createRouteWithAuth(libs, route),
server
);
const routeDefinition = {
path,
validate,
options,
};
switch (method) {
case 'GET':
router.versioned
.get({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: INITIAL_REST_VERSION,
validate: {
request: {
body: validate ? validate?.body : undefined,
},
response: {
200: {
body: DynamicSettingsSchema,
},
},
},
},
handler
);
break;
case 'PUT':
router.versioned
.put({
access: 'public',
path: routeDefinition.path,
options: {
tags: options?.tags,
},
})
.addVersion(
{
version: INITIAL_REST_VERSION,
validate: {
request: {
body: validate ? validate?.body : undefined,
},
response: {
200: {
body: DynamicSettingsSchema,
},
},
},
},
handler
);
break;
default:
throw new Error(`Handler for method ${method} is not defined`);
}
});
const {
alerting: { registerType },
} = plugins;

View file

@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) {
it('creates rule when settings are configured', async () => {
await supertest
.post(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send({
heartbeatIndices: 'heartbeat-*',
@ -76,7 +76,7 @@ export default function ({ getService }: FtrProviderContext) {
it('updates rules when settings are updated', async () => {
await supertest
.post(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send({
heartbeatIndices: 'heartbeat-*',

View file

@ -29,15 +29,24 @@ export default function ({ getService }: FtrProviderContext) {
defaultConnectors: [],
};
const postResponse = await supertest
.post(API_URLS.DYNAMIC_SETTINGS)
.put(API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send(newSettings);
expect(postResponse.body).to.eql({ success: true });
expect(postResponse.body).to.eql({
heartbeatIndices: 'myIndex1*',
certExpirationThreshold: 5,
certAgeThreshold: 15,
defaultConnectors: [],
defaultEmail: { to: [], cc: [], bcc: [] },
});
expect(postResponse.status).to.eql(200);
const getResponse = await supertest.get(API_URLS.DYNAMIC_SETTINGS);
expect(getResponse.body).to.eql(newSettings);
expect(getResponse.body).to.eql({
...newSettings,
defaultEmail: { to: [], cc: [], bcc: [] },
});
expect(isRight(DynamicSettingsCodec.decode(getResponse.body))).to.be.ok();
});
});