kibana/x-pack/test/spaces_api_integration/common/suites/resolve_copy_to_space_conflicts.ts
Spencer f466ebf1a3
[esArchiver] drop support for --dir, use repo-relative paths instead (#101345)
Co-authored-by: spalger <spalger@users.noreply.github.com>
2021-06-08 17:37:42 -04:00

574 lines
20 KiB
TypeScript

/*
* 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 expect from '@kbn/expect';
import { SuperTest } from 'supertest';
import { EsArchiver } from '@kbn/es-archiver';
import { SavedObject } from 'src/core/server';
import { DEFAULT_SPACE_ID } from '../../../../plugins/spaces/common/constants';
import { CopyResponse } from '../../../../plugins/spaces/server/lib/copy_to_spaces';
import { getUrlPrefix } from '../lib/space_test_utils';
import { DescribeFn, TestDefinitionAuthentication } from '../lib/types';
type TestResponse = Record<string, any>;
interface ResolveCopyToSpaceTest {
statusCode: number;
response: (resp: TestResponse) => Promise<void>;
}
interface ResolveCopyToSpaceMultiNamespaceTest extends ResolveCopyToSpaceTest {
testTitle: string;
objects: Array<Record<string, any>>;
retries: Record<string, any>;
}
interface ResolveCopyToSpaceTests {
withReferencesNotOverwriting: ResolveCopyToSpaceTest;
withReferencesOverwriting: ResolveCopyToSpaceTest;
withoutReferencesOverwriting: ResolveCopyToSpaceTest;
withoutReferencesNotOverwriting: ResolveCopyToSpaceTest;
nonExistentSpace: ResolveCopyToSpaceTest;
multiNamespaceTestCases: () => ResolveCopyToSpaceMultiNamespaceTest[];
}
interface ResolveCopyToSpaceTestDefinition {
user?: TestDefinitionAuthentication;
spaceId?: string;
tests: ResolveCopyToSpaceTests;
}
const NON_EXISTENT_SPACE_ID = 'non_existent_space';
const getDestinationSpace = (originSpaceId?: string) => {
if (!originSpaceId || originSpaceId === DEFAULT_SPACE_ID) {
return 'space_1';
}
return DEFAULT_SPACE_ID;
};
export function resolveCopyToSpaceConflictsSuite(
esArchiver: EsArchiver,
supertestWithAuth: SuperTest<any>,
supertestWithoutAuth: SuperTest<any>
) {
const getVisualizationAtSpace = async (spaceId: string): Promise<SavedObject<any>> => {
return supertestWithAuth
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/visualization/cts_vis_3`)
.then((response: any) => response.body);
};
const getDashboardAtSpace = async (spaceId: string): Promise<SavedObject<any>> => {
return supertestWithAuth
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/cts_dashboard`)
.then((response: any) => response.body);
};
const getObjectsAtSpace = async (
spaceId: string
): Promise<[SavedObject<any>, SavedObject<any>]> => {
const dashboard = await getDashboardAtSpace(spaceId);
const visualization = await getVisualizationAtSpace(spaceId);
return [dashboard, visualization];
};
const createExpectOverriddenResponseWithReferences = (sourceSpaceId: string) => async (
response: TestResponse
) => {
const destination = getDestinationSpace(sourceSpaceId);
const result = response.body;
expect(result).to.eql({
[destination]: {
success: true,
successCount: 1,
successResults: [
{
id: 'cts_vis_3',
type: 'visualization',
meta: {
title: `CTS vis 3 from ${sourceSpaceId} space`,
icon: 'visualizeApp',
},
overwrite: true,
},
],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destination);
expect(dashboard.attributes.title).to.eql(
`This is the ${destination} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${sourceSpaceId} space`);
};
const createExpectOverriddenResponseWithoutReferences = (
sourceSpaceId: string,
destinationSpaceId: string = getDestinationSpace(sourceSpaceId)
) => async (response: TestResponse) => {
const result = response.body;
expect(result).to.eql({
[destinationSpaceId]: {
success: true,
successCount: 1,
successResults: [
{
id: 'cts_dashboard',
type: 'dashboard',
meta: {
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
icon: 'dashboardApp',
},
overwrite: true,
},
],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId);
expect(dashboard.attributes.title).to.eql(
`This is the ${sourceSpaceId} test space CTS dashboard`
);
if (destinationSpaceId === NON_EXISTENT_SPACE_ID) {
expect((visualization as any).statusCode).to.eql(404);
} else {
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destinationSpaceId} space`);
}
};
const createExpectNonOverriddenResponseWithReferences = (sourceSpaceId: string) => async (
response: TestResponse
) => {
const destination = getDestinationSpace(sourceSpaceId);
const result = response.body;
expect(result).to.eql({
[destination]: {
success: false,
successCount: 0,
errors: [
{
error: { type: 'conflict' },
id: 'cts_vis_3',
title: `CTS vis 3 from ${sourceSpaceId} space`,
meta: {
title: `CTS vis 3 from ${sourceSpaceId} space`,
icon: 'visualizeApp',
},
type: 'visualization',
},
],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destination);
expect(dashboard.attributes.title).to.eql(
`This is the ${destination} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
};
const createExpectNonOverriddenResponseWithoutReferences = (sourceSpaceId: string) => async (
response: TestResponse
) => {
const destination = getDestinationSpace(sourceSpaceId);
const result = response.body;
expect(result).to.eql({
[destination]: {
success: false,
successCount: 0,
errors: [
{
error: { type: 'conflict' },
id: 'cts_dashboard',
type: 'dashboard',
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
meta: {
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
icon: 'dashboardApp',
},
},
],
},
});
const [dashboard, visualization] = await getObjectsAtSpace(destination);
expect(dashboard.attributes.title).to.eql(
`This is the ${destination} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
};
const expectRouteForbiddenResponse = async (resp: TestResponse) => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: 'Forbidden',
});
};
const expectRouteNotFoundResponse = async (resp: TestResponse) => {
expect(resp.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
};
const createExpectUnauthorizedAtSpaceWithReferencesResult = (
spaceId: string = DEFAULT_SPACE_ID
) => async (resp: TestResponse) => {
const destination = getDestinationSpace(spaceId);
const result = resp.body as CopyResponse;
expect(result).to.eql({
[destination]: {
success: false,
successCount: 0,
errors: [
{
statusCode: 403,
error: 'Forbidden',
message: 'Unable to bulk_get index-pattern',
},
],
},
} as CopyResponse);
// Query ES to ensure that nothing was copied
const [dashboard, visualization] = await getObjectsAtSpace(destination);
expect(dashboard.attributes.title).to.eql(
`This is the ${destination} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
};
const createExpectReadonlyAtSpaceWithReferencesResult = (
spaceId: string = DEFAULT_SPACE_ID
) => async (resp: TestResponse) => {
const destination = getDestinationSpace(spaceId);
const result = resp.body as CopyResponse;
expect(result).to.eql({
[destination]: {
success: false,
successCount: 0,
errors: [
{
statusCode: 403,
error: 'Forbidden',
message: 'Unable to bulk_create visualization',
},
],
},
} as CopyResponse);
// Query ES to ensure that nothing was copied
const [dashboard, visualization] = await getObjectsAtSpace(destination);
expect(dashboard.attributes.title).to.eql(
`This is the ${destination} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destination} space`);
};
const createExpectUnauthorizedAtSpaceWithoutReferencesResult = (
sourceSpaceId: string = DEFAULT_SPACE_ID,
destinationSpaceId: string = getDestinationSpace(sourceSpaceId)
) => async (resp: TestResponse) => {
const result = resp.body as CopyResponse;
expect(result).to.eql({
[destinationSpaceId]: {
success: false,
successCount: 0,
errors: [
{
statusCode: 403,
error: 'Forbidden',
message: 'Unable to bulk_create dashboard',
},
],
},
} as CopyResponse);
// Query ES to ensure that nothing was copied
const [dashboard, visualization] = await getObjectsAtSpace(destinationSpaceId);
if (destinationSpaceId === NON_EXISTENT_SPACE_ID) {
expect((dashboard as any).statusCode).to.eql(404);
expect((visualization as any).statusCode).to.eql(404);
} else {
expect(dashboard.attributes.title).to.eql(
`This is the ${destinationSpaceId} test space CTS dashboard`
);
expect(visualization.attributes.title).to.eql(`CTS vis 3 from ${destinationSpaceId} space`);
}
};
/**
* Creates test cases for multi-namespace saved object types.
* Note: these are written with the assumption that test data will only be reloaded between each group of test cases, *not* before every
* single test case. This saves time during test execution.
*/
const createMultiNamespaceTestCases = (
spaceId: string,
outcome: 'authorized' | 'unauthorizedRead' | 'unauthorizedWrite' | 'noAccess' = 'authorized'
) => (): ResolveCopyToSpaceMultiNamespaceTest[] => {
// the status code of the HTTP response differs depending on the error type
// a 403 error actually comes back as an HTTP 200 response
const statusCode = outcome === 'noAccess' ? 403 : 200;
const type = 'sharedtype';
const exactMatchId = 'each_space';
const inexactMatchId = `conflict_1_${spaceId}`;
const ambiguousConflictId = `conflict_2_${spaceId}`;
const createRetries = (overwriteRetry: Record<string, any>) => ({ space_2: [overwriteRetry] });
const getResult = (response: TestResponse) => (response.body as CopyResponse).space_2;
const expectSavedObjectForbiddenResponse = (response: TestResponse) => {
expect(response.body).to.eql({
space_2: {
success: false,
successCount: 0,
errors: [
{ statusCode: 403, error: 'Forbidden', message: `Unable to bulk_create sharedtype` },
],
},
});
};
const expectSavedObjectSuccessResponse = (
response: TestResponse,
id: string,
destinationId?: string
) => {
const { success, successCount, successResults, errors } = getResult(response);
expect(success).to.eql(true);
expect(successCount).to.eql(1);
expect(errors).to.be(undefined);
const title =
id === exactMatchId
? 'A shared saved-object in the default, space_1, and space_2 spaces'
: 'A shared saved-object in one space';
const meta = { title, icon: 'beaker' };
expect(successResults).to.eql([
{ type, id, meta, overwrite: true, ...(destinationId && { destinationId }) },
]);
};
return [
{
testTitle: 'copying with an exact match conflict',
objects: [{ type, id: exactMatchId }],
retries: createRetries({ type, id: exactMatchId, overwrite: true }),
statusCode,
response: async (response: TestResponse) => {
if (outcome === 'authorized') {
expectSavedObjectSuccessResponse(response, exactMatchId);
} else if (outcome === 'noAccess') {
expectRouteForbiddenResponse(response);
} else {
// unauthorized read/write
expectSavedObjectForbiddenResponse(response);
}
},
},
{
testTitle: 'copying with an inexact match conflict',
objects: [{ type, id: inexactMatchId }],
retries: createRetries({
type,
id: inexactMatchId,
overwrite: true,
destinationId: 'conflict_1_space_2',
}),
statusCode,
response: async (response: TestResponse) => {
if (outcome === 'authorized') {
expectSavedObjectSuccessResponse(response, inexactMatchId, 'conflict_1_space_2');
} else if (outcome === 'noAccess') {
expectRouteForbiddenResponse(response);
} else {
// unauthorized read/write
expectSavedObjectForbiddenResponse(response);
}
},
},
{
testTitle: 'copying with an ambiguous conflict',
objects: [{ type, id: ambiguousConflictId }],
retries: createRetries({
type,
id: ambiguousConflictId,
overwrite: true,
destinationId: 'conflict_2_space_2',
}),
statusCode,
response: async (response: TestResponse) => {
if (outcome === 'authorized') {
expectSavedObjectSuccessResponse(response, ambiguousConflictId, 'conflict_2_space_2');
} else if (outcome === 'noAccess') {
expectRouteForbiddenResponse(response);
} else {
// unauthorized read/write
expectSavedObjectForbiddenResponse(response);
}
},
},
];
};
const makeResolveCopyToSpaceConflictsTest = (describeFn: DescribeFn) => (
description: string,
{ user = {}, spaceId = DEFAULT_SPACE_ID, tests }: ResolveCopyToSpaceTestDefinition
) => {
describeFn(description, () => {
before(() => {
// test data only allows for the following spaces as the copy origin
expect(['default', 'space_1']).to.contain(spaceId);
});
describe('single-namespace types', () => {
beforeEach(() =>
esArchiver.load(
'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
)
);
afterEach(() =>
esArchiver.unload(
'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
)
);
const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' };
const visualizationObject = { type: 'visualization', id: 'cts_vis_3' };
it(`should return ${tests.withReferencesNotOverwriting.statusCode} when not overwriting, with references`, async () => {
const destination = getDestinationSpace(spaceId);
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({
objects: [dashboardObject],
includeReferences: true,
createNewCopies: false,
retries: { [destination]: [{ ...visualizationObject, overwrite: false }] },
})
.expect(tests.withReferencesNotOverwriting.statusCode)
.then(tests.withReferencesNotOverwriting.response);
});
it(`should return ${tests.withReferencesOverwriting.statusCode} when overwriting, with references`, async () => {
const destination = getDestinationSpace(spaceId);
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({
objects: [dashboardObject],
includeReferences: true,
createNewCopies: false,
retries: { [destination]: [{ ...visualizationObject, overwrite: true }] },
})
.expect(tests.withReferencesOverwriting.statusCode)
.then(tests.withReferencesOverwriting.response);
});
it(`should return ${tests.withoutReferencesOverwriting.statusCode} when overwriting, without references`, async () => {
const destination = getDestinationSpace(spaceId);
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
})
.expect(tests.withoutReferencesOverwriting.statusCode)
.then(tests.withoutReferencesOverwriting.response);
});
it(`should return ${tests.withoutReferencesNotOverwriting.statusCode} when not overwriting, without references`, async () => {
const destination = getDestinationSpace(spaceId);
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: false }] },
})
.expect(tests.withoutReferencesNotOverwriting.statusCode)
.then(tests.withoutReferencesNotOverwriting.response);
});
it(`should return ${tests.nonExistentSpace.statusCode} when resolving within a non-existent space`, async () => {
const destination = NON_EXISTENT_SPACE_ID;
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
})
.expect(tests.nonExistentSpace.statusCode)
.then(tests.nonExistentSpace.response);
});
});
const includeReferences = false;
const createNewCopies = false;
describe(`multi-namespace types with "overwrite" retry`, () => {
before(() =>
esArchiver.load(
'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
)
);
after(() =>
esArchiver.unload(
'x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces'
)
);
const testCases = tests.multiNamespaceTestCases();
testCases.forEach(({ testTitle, objects, retries, statusCode, response }) => {
it(`should return ${statusCode} when ${testTitle}`, async () => {
return supertestWithoutAuth
.post(`${getUrlPrefix(spaceId)}/api/spaces/_resolve_copy_saved_objects_errors`)
.auth(user.username, user.password)
.send({ objects, includeReferences, createNewCopies, retries })
.expect(statusCode)
.then(response);
});
});
});
});
};
const resolveCopyToSpaceConflictsTest = makeResolveCopyToSpaceConflictsTest(describe);
// @ts-ignore
resolveCopyToSpaceConflictsTest.only = makeResolveCopyToSpaceConflictsTest(describe.only);
return {
resolveCopyToSpaceConflictsTest,
expectRouteForbiddenResponse,
expectRouteNotFoundResponse,
createExpectOverriddenResponseWithReferences,
createExpectOverriddenResponseWithoutReferences,
createExpectNonOverriddenResponseWithReferences,
createExpectNonOverriddenResponseWithoutReferences,
createExpectUnauthorizedAtSpaceWithReferencesResult,
createExpectReadonlyAtSpaceWithReferencesResult,
createExpectUnauthorizedAtSpaceWithoutReferencesResult,
createMultiNamespaceTestCases,
originSpaces: ['default', 'space_1'],
NON_EXISTENT_SPACE_ID,
};
}