Skip to content

Commit

Permalink
Fix serverless cases tests for MKI runs (#166080)
Browse files Browse the repository at this point in the history
## Summary

This PR fixes the serverless cases tests for MKI runs against
Observability and Security projects.

### The issues we were seeing

There were actually two issues where things worked locally but not in
MKI:
* A hard-coded `elastic_serverless` user
* Deleting from system indices directly

### How this PR solves them

* Replace the hard-coded `elastic_serverless` user with the one obtained
via the test config
* Replace deletion from system indices with saved object API `clean`
calls

### Other changes

I've noticed, that the tests are using local helper methods, but
observability and security helper methods were 99.9% the same code. In
order to make the code easier to re-use and also allow usage of other
services from within the helper methods, we recommend putting helper
methods into services. I've refactored the code to do this:

* Create a new serverless API integrations service `svlCases`
* Combine
`x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/api.ts`
and
`x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/api.ts`
into the `svlCases.api` service
* Combine
`x-pack/test_serverless/api_integration/test_suites/observability/cases/helpers/omit.ts`
and
`x-pack/test_serverless/api_integration/test_suites/security/cases/helpers/omit.ts`
into the `svlCases.omit` service
* Clean up dependencies a bit ( e.g. no need to pass `supertest` around
since `svlCases.api` can load the supertest service on its own now)
  • Loading branch information
pheyos authored Sep 8, 2023
1 parent 1617a39 commit 7b90ae2
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 688 deletions.
2 changes: 2 additions & 0 deletions x-pack/test_serverless/api_integration/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SvlCommonApiServiceProvider } from './svl_common_api';
import { AlertingApiProvider } from './alerting_api';
import { SamlToolsProvider } from './saml_tools';
import { DataViewApiProvider } from './data_view_api';
import { SvlCasesServiceProvider } from './svl_cases';

export const services = {
...xpackApiIntegrationServices,
Expand All @@ -23,6 +24,7 @@ export const services = {
alertingApi: AlertingApiProvider,
samlTools: SamlToolsProvider,
dataViewApi: DataViewApiProvider,
svlCases: SvlCasesServiceProvider,
};

export type InheritedFtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
Expand Down
233 changes: 233 additions & 0 deletions x-pack/test_serverless/api_integration/services/svl_cases/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/*
* 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 type SuperTest from 'supertest';
import { CASES_URL } from '@kbn/cases-plugin/common';
import { Case, CaseSeverity, CaseStatuses } from '@kbn/cases-plugin/common/types/domain';
import type { CasePostRequest } from '@kbn/cases-plugin/common/types/api';
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';
import { CasesFindResponse } from '@kbn/cases-plugin/common/types/api';
import { kbnTestConfig, kibanaTestSuperuserServerless } from '@kbn/test';
import { FtrProviderContext } from '../../ftr_provider_context';

export function SvlCasesApiServiceProvider({ getService }: FtrProviderContext) {
const kbnServer = getService('kibanaServer');
const supertest = getService('supertest');

interface User {
username: string;
password: string;
description?: string;
roles: string[];
}

const superUser: User = {
username: 'superuser',
password: 'superuser',
roles: ['superuser'],
};

const defaultUser = {
email: null,
full_name: null,
username: kbnTestConfig.getUrlParts(kibanaTestSuperuserServerless).username,
};

/**
* A null filled user will occur when the security plugin is disabled
*/
const nullUser = { email: null, full_name: null, username: null };

const findCommon = {
page: 1,
per_page: 20,
total: 0,
count_open_cases: 0,
count_closed_cases: 0,
count_in_progress_cases: 0,
};

const findCasesResp: CasesFindResponse = {
...findCommon,
cases: [],
};

return {
setupAuth({
apiCall,
headers,
auth,
}: {
apiCall: SuperTest.Test;
headers: Record<string, unknown>;
auth?: { user: User; space: string | null } | null;
}): SuperTest.Test {
if (!Object.hasOwn(headers, 'Cookie') && auth != null) {
return apiCall.auth(auth.user.username, auth.user.password);
}

return apiCall;
},

getSpaceUrlPrefix(spaceId: string | undefined | null) {
return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``;
},

async deleteAllCaseItems() {
await Promise.all([
this.deleteCasesByESQuery(),
this.deleteCasesUserActions(),
this.deleteComments(),
this.deleteConfiguration(),
this.deleteMappings(),
]);
},

async deleteCasesUserActions(): Promise<void> {
await kbnServer.savedObjects.clean({ types: ['cases-user-actions'] });
},

async deleteCasesByESQuery(): Promise<void> {
await kbnServer.savedObjects.clean({ types: ['cases'] });
},

async deleteComments(): Promise<void> {
await kbnServer.savedObjects.clean({ types: ['cases-comments'] });
},

async deleteConfiguration(): Promise<void> {
await kbnServer.savedObjects.clean({ types: ['cases-configure'] });
},

async deleteMappings(): Promise<void> {
await kbnServer.savedObjects.clean({ types: ['cases-connector-mappings'] });
},

/**
* Return a request for creating a case.
*/
getPostCaseRequest(owner: string, req?: Partial<CasePostRequest>): CasePostRequest {
return {
...this.getPostCaseReq(owner),
...req,
};
},

postCaseResp(owner: string, id?: string | null, req?: CasePostRequest): Partial<Case> {
const request = req ?? this.getPostCaseReq(owner);
return {
...request,
...(id != null ? { id } : {}),
comments: [],
duration: null,
severity: request.severity ?? CaseSeverity.LOW,
totalAlerts: 0,
totalComment: 0,
closed_by: null,
created_by: defaultUser,
external_service: null,
status: CaseStatuses.open,
updated_by: null,
category: null,
};
},

async createCase(
params: CasePostRequest,
expectedHttpCode: number = 200,
auth: { user: User; space: string | null } | null = { user: superUser, space: null },
headers: Record<string, unknown> = {}
): Promise<Case> {
const apiCall = supertest.post(`${CASES_URL}`);

this.setupAuth({ apiCall, headers, auth });

const response = await apiCall
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo')
.set(headers)
.send(params)
.expect(expectedHttpCode);

return response.body;
},

async findCases({
query = {},
expectedHttpCode = 200,
auth = { user: superUser, space: null },
}: {
query?: Record<string, unknown>;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
}): Promise<CasesFindResponse> {
const { body: res } = await supertest
.get(`${this.getSpaceUrlPrefix(auth.space)}${CASES_URL}/_find`)
.auth(auth.user.username, auth.user.password)
.query({ sortOrder: 'asc', ...query })
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo')
.send()
.expect(expectedHttpCode);

return res;
},

async getCase({
caseId,
includeComments = false,
expectedHttpCode = 200,
auth = { user: superUser, space: null },
}: {
caseId: string;
includeComments?: boolean;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
}): Promise<Case> {
const { body: theCase } = await supertest
.get(
`${this.getSpaceUrlPrefix(
auth?.space
)}${CASES_URL}/${caseId}?includeComments=${includeComments}`
)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo')
.auth(auth.user.username, auth.user.password)
.expect(expectedHttpCode);

return theCase;
},

getFindCasesResp() {
return findCasesResp;
},

getPostCaseReq(owner: string): CasePostRequest {
return {
description: 'This is a brand new case of a bad meanie defacing data',
title: 'Super Bad Observability Issue',
tags: ['defacement'],
severity: CaseSeverity.LOW,
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
owner,
assignees: [],
};
},

getNullUser() {
return nullUser;
},
};
}
21 changes: 21 additions & 0 deletions x-pack/test_serverless/api_integration/services/svl_cases/index.ts
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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { FtrProviderContext } from '../../ftr_provider_context';

import { SvlCasesApiServiceProvider } from './api';
import { SvlCasesOmitServiceProvider } from './omit';

export function SvlCasesServiceProvider(context: FtrProviderContext) {
const api = SvlCasesApiServiceProvider(context);
const omit = SvlCasesOmitServiceProvider(context);

return {
api,
omit,
};
}
53 changes: 53 additions & 0 deletions x-pack/test_serverless/api_integration/services/svl_cases/omit.ts
Original file line number Diff line number Diff line change
@@ -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 { Case, Attachment } from '@kbn/cases-plugin/common/types/domain';
import { omit } from 'lodash';
import { FtrProviderContext } from '../../ftr_provider_context';

export function SvlCasesOmitServiceProvider({}: FtrProviderContext) {
interface CommonSavedObjectAttributes {
id?: string | null;
created_at?: string | null;
updated_at?: string | null;
version?: string | null;
[key: string]: unknown;
}

const savedObjectCommonAttributes = ['created_at', 'updated_at', 'version', 'id'];

return {
removeServerGeneratedPropertiesFromObject<T extends object, K extends keyof T>(
object: T,
keys: K[]
): Omit<T, K> {
return omit<T, K>(object, keys);
},

removeServerGeneratedPropertiesFromSavedObject<T extends CommonSavedObjectAttributes>(
attributes: T,
keys: Array<keyof T> = []
): Omit<T, typeof savedObjectCommonAttributes[number] | typeof keys[number]> {
return this.removeServerGeneratedPropertiesFromObject(attributes, [
...savedObjectCommonAttributes,
...keys,
]);
},

removeServerGeneratedPropertiesFromCase(theCase: Case): Partial<Case> {
return this.removeServerGeneratedPropertiesFromSavedObject<Case>(theCase, ['closed_at']);
},

removeServerGeneratedPropertiesFromComments(
comments: Attachment[] | undefined
): Array<Partial<Attachment>> | undefined {
return comments?.map((comment) => {
return this.removeServerGeneratedPropertiesFromSavedObject<Attachment>(comment, []);
});
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@

import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import {
findCases,
createCase,
deleteAllCaseItems,
postCaseReq,
findCasesResp,
} from './helpers/api';

export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const es = getService('es');
const svlCases = getService('svlCases');

let findCasesResp: any;
let postCaseReq: any;

describe('find_cases', () => {
before(async () => {
findCasesResp = svlCases.api.getFindCasesResp();
postCaseReq = svlCases.api.getPostCaseReq('observability');
});

afterEach(async () => {
await deleteAllCaseItems(es);
await svlCases.api.deleteAllCaseItems();
});

it('should return empty response', async () => {
const cases = await findCases({ supertest });
const cases = await svlCases.api.findCases({});
expect(cases).to.eql(findCasesResp);
});

it('should return cases', async () => {
const a = await createCase(supertest, postCaseReq);
const b = await createCase(supertest, postCaseReq);
const c = await createCase(supertest, postCaseReq);
const a = await svlCases.api.createCase(postCaseReq);
const b = await svlCases.api.createCase(postCaseReq);
const c = await svlCases.api.createCase(postCaseReq);

const cases = await findCases({ supertest });
const cases = await svlCases.api.findCases({});

expect(cases).to.eql({
...findCasesResp,
Expand All @@ -45,12 +45,14 @@ export default ({ getService }: FtrProviderContext): void => {
});

it('returns empty response when trying to find cases with owner as cases', async () => {
const cases = await findCases({ supertest, query: { owner: 'cases' } });
const cases = await svlCases.api.findCases({ query: { owner: 'cases' } });
expect(cases).to.eql(findCasesResp);
});

it('returns empty response when trying to find cases with owner as securitySolution', async () => {
const cases = await findCases({ supertest, query: { owner: 'securitySolution' } });
const cases = await svlCases.api.findCases({
query: { owner: 'securitySolution' },
});
expect(cases).to.eql(findCasesResp);
});
});
Expand Down
Loading

0 comments on commit 7b90ae2

Please sign in to comment.