Remove ability to implicitly find across all types (#23198)

This commit is contained in:
Larry Gregory 2018-09-19 07:22:43 -04:00 committed by GitHub
parent c940c6d111
commit 33acd60f9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 109 additions and 334 deletions

View file

@ -11,13 +11,12 @@ saved objects by various conditions.
`GET /api/saved_objects/_find`
==== Query Parameters
`type` (required)::
(array|string) The saved object type(s) that the response should be limited to
`per_page` (optional)::
(number) The number of objects to return per page
`page` (optional)::
(number) The page of objects to return
`type` (optional)::
(array|string) The saved object type(s) that the response should be limited to
`search` (optional)::
(string) A {ref}/query-dsl-simple-query-string-query.html[simple_query_string] Elasticsearch query to filter the objects in the response
`search_fields` (optional)::

View file

@ -29,7 +29,7 @@ export const createFindRoute = (prereqs) => ({
query: Joi.object().keys({
per_page: Joi.number().min(0).default(20),
page: Joi.number().min(0).default(1),
type: Joi.array().items(Joi.string()).single(),
type: Joi.array().items(Joi.string()).single().required(),
search: Joi.string().allow('').optional(),
search_fields: Joi.array().items(Joi.string()).single(),
sort_field: Joi.array().items(Joi.string()).single(),

View file

@ -44,12 +44,28 @@ describe('GET /api/saved_objects/_find', () => {
savedObjectsClient.find.resetHistory();
});
it('formats successful response', async () => {
it('returns with status 400 when type is missing', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find'
};
const { payload, statusCode } = await server.inject(request);
expect(statusCode).toEqual(400);
expect(JSON.parse(payload)).toMatchObject({
statusCode: 400,
error: 'Bad Request',
message: 'child "type" fails because ["type" is required]',
});
});
it('formats successful response', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find?type=index-pattern'
};
const clientResponse = {
total: 2,
data: [
@ -81,7 +97,7 @@ describe('GET /api/saved_objects/_find', () => {
it('calls upon savedObjectClient.find with defaults', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find'
url: '/api/saved_objects/_find?type=foo&type=bar'
};
await server.inject(request);
@ -89,13 +105,13 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find.calledOnce).toBe(true);
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({ perPage: 20, page: 1 });
expect(options).toEqual({ perPage: 20, page: 1, type: ['foo', 'bar'] });
});
it('accepts the query parameter page/per_page', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find?per_page=10&page=50'
url: '/api/saved_objects/_find?type=foo&per_page=10&page=50'
};
await server.inject(request);
@ -103,13 +119,13 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find.calledOnce).toBe(true);
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({ perPage: 10, page: 50 });
expect(options).toEqual({ perPage: 10, page: 50, type: ['foo'] });
});
it('accepts the query parameter search_fields', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find?search_fields=title'
url: '/api/saved_objects/_find?type=foo&search_fields=title'
};
await server.inject(request);
@ -117,13 +133,13 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find.calledOnce).toBe(true);
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'] });
expect(options).toEqual({ perPage: 20, page: 1, searchFields: ['title'], type: ['foo'] });
});
it('accepts the query parameter fields as a string', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find?fields=title'
url: '/api/saved_objects/_find?type=foo&fields=title'
};
await server.inject(request);
@ -131,13 +147,13 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find.calledOnce).toBe(true);
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'] });
expect(options).toEqual({ perPage: 20, page: 1, fields: ['title'], type: ['foo'] });
});
it('accepts the query parameter fields as an array', async () => {
const request = {
method: 'GET',
url: '/api/saved_objects/_find?fields=title&fields=description'
url: '/api/saved_objects/_find?type=foo&fields=title&fields=description'
};
await server.inject(request);
@ -146,7 +162,7 @@ describe('GET /api/saved_objects/_find', () => {
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({
perPage: 20, page: 1, fields: ['title', 'description']
perPage: 20, page: 1, fields: ['title', 'description'], type: ['foo']
});
});
@ -161,7 +177,7 @@ describe('GET /api/saved_objects/_find', () => {
expect(savedObjectsClient.find.calledOnce).toBe(true);
const options = savedObjectsClient.find.getCall(0).args[0];
expect(options).toEqual({ perPage: 20, page: 1, type: [ 'index-pattern' ] });
expect(options).toEqual({ perPage: 20, page: 1, type: ['index-pattern'] });
});
it('accepts the query parameter type as an array', async () => {

View file

@ -251,6 +251,10 @@ export class SavedObjectsRepository {
fields,
} = options;
if (!type) {
throw new TypeError(`options.type must be a string or an array of strings`);
}
if (searchFields && !Array.isArray(searchFields)) {
throw new TypeError('options.searchFields must be an array');
}

View file

@ -385,7 +385,7 @@ describe('SavedObjectsRepository', () => {
});
describe('#delete', () => {
it('throws notFound when ES is unable to find the document', async () => {
it('throws notFound when ES is unable to find the document', async () => {
expect.assertions(1);
callAdminCluster.returns(Promise.resolve({
@ -394,7 +394,7 @@ describe('SavedObjectsRepository', () => {
try {
await savedObjectsRepository.delete('index-pattern', 'logstash-*');
} catch(e) {
} catch (e) {
expect(e.output.statusCode).toEqual(404);
}
});
@ -423,9 +423,15 @@ describe('SavedObjectsRepository', () => {
callAdminCluster.returns(searchResults);
});
it('requires type to be defined', async () => {
await expect(savedObjectsRepository.find({})).rejects.toThrow(/options\.type must be/);
sinon.assert.notCalled(callAdminCluster);
sinon.assert.notCalled(onBeforeWrite);
});
it('requires searchFields be an array if defined', async () => {
try {
await savedObjectsRepository.find({ searchFields: 'string' });
await savedObjectsRepository.find({ type: 'foo', searchFields: 'string' });
throw new Error('expected find() to reject');
} catch (error) {
sinon.assert.notCalled(callAdminCluster);
@ -436,7 +442,7 @@ describe('SavedObjectsRepository', () => {
it('requires fields be an array if defined', async () => {
try {
await savedObjectsRepository.find({ fields: 'string' });
await savedObjectsRepository.find({ type: 'foo', fields: 'string' });
throw new Error('expected find() to reject');
} catch (error) {
sinon.assert.notCalled(callAdminCluster);
@ -461,7 +467,7 @@ describe('SavedObjectsRepository', () => {
it('merges output of getSearchDsl into es request body', async () => {
getSearchDsl.returns({ query: 1, aggregations: 2 });
await savedObjectsRepository.find();
await savedObjectsRepository.find({ type: 'foo' });
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.notCalled(onBeforeWrite);
sinon.assert.calledWithExactly(callAdminCluster, 'search', sinon.match({
@ -475,7 +481,7 @@ describe('SavedObjectsRepository', () => {
it('formats Elasticsearch response', async () => {
const count = searchResults.hits.hits.length;
const response = await savedObjectsRepository.find();
const response = await savedObjectsRepository.find({ type: 'foo' });
expect(response.total).toBe(count);
expect(response.saved_objects).toHaveLength(count);
@ -492,7 +498,7 @@ describe('SavedObjectsRepository', () => {
});
it('accepts per_page/page', async () => {
await savedObjectsRepository.find({ perPage: 10, page: 6 });
await savedObjectsRepository.find({ type: 'foo', perPage: 10, page: 6 });
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
@ -504,12 +510,12 @@ describe('SavedObjectsRepository', () => {
});
it('can filter by fields', async () => {
await savedObjectsRepository.find({ fields: ['title'] });
await savedObjectsRepository.find({ type: 'foo', fields: ['title'] });
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.calledWithExactly(callAdminCluster, sinon.match.string, sinon.match({
_source: [
'*.title', 'type', 'title'
'foo.title', 'type', 'title'
]
}));

View file

@ -31,8 +31,8 @@ export function getSearchDsl(mappings, options = {}) {
sortOrder
} = options;
if (!type && sortField) {
throw Boom.notAcceptable('Cannot sort without filtering by type');
if (!type) {
throw Boom.notAcceptable('type must be specified');
}
if (sortOrder && !sortField) {

View file

@ -27,13 +27,13 @@ describe('getSearchDsl', () => {
afterEach(() => sandbox.restore());
describe('validation', () => {
it('throws when sortField is passed without type', () => {
it('throws when type is not specified', () => {
expect(() => {
getSearchDsl({}, {
type: undefined,
sortField: 'title'
});
}).toThrowError(/sort without .+ type/);
}).toThrowError(/type must be specified/);
});
it('throws when sortOrder without sortField', () => {
expect(() => {
@ -89,7 +89,7 @@ describe('getSearchDsl', () => {
it('returns combination of getQueryParams and getSortingParams', () => {
sandbox.stub(queryParamsNS, 'getQueryParams').returns({ a: 'a' });
sandbox.stub(sortParamsNS, 'getSortingParams').returns({ b: 'b' });
expect(getSearchDsl({})).toEqual({ a: 'a', b: 'b' });
expect(getSearchDsl(null, { type: 'foo' })).toEqual({ a: 'a', b: 'b' });
});
});
});

View file

@ -72,7 +72,7 @@ test(`#find`, async () => {
};
const client = new SavedObjectsClient(mockRepository);
const options = {};
const options = { type: 'foo' };
const result = await client.find(options);
expect(mockRepository.find).toHaveBeenCalledWith(options);

View file

@ -140,6 +140,25 @@ export default function ({ getService }) {
));
});
describe('missing type', () => {
it('should return 400', async () => (
await supertest
.get('/api/saved_objects/_find')
.expect(400)
.then(resp => {
expect(resp.body).to.eql({
error: 'Bad Request',
message: 'child "type" fails because ["type" is required]',
statusCode: 400,
validation: {
keys: ['type'],
source: 'query'
}
});
})
));
});
describe('page beyond total', () => {
it('should return 200 with empty response', async () => (
await supertest

View file

@ -139,7 +139,6 @@ export const security = (kibana) => new kibana.Plugin({
errors: savedObjects.SavedObjectsClient.errors,
checkPrivileges,
auditLogger,
savedObjectTypes: savedObjects.types,
actions: authorization.actions,
});
});

View file

@ -15,7 +15,6 @@ export class SecureSavedObjectsClient {
callWithRequestRepository,
checkPrivileges,
auditLogger,
savedObjectTypes,
actions,
} = options;
@ -24,7 +23,6 @@ export class SecureSavedObjectsClient {
this._callWithRequestRepository = callWithRequestRepository;
this._checkPrivileges = checkPrivileges;
this._auditLogger = auditLogger;
this._savedObjectTypes = savedObjectTypes;
this._actions = actions;
}
@ -57,11 +55,12 @@ export class SecureSavedObjectsClient {
}
async find(options = {}) {
if (options.type) {
return await this._findWithTypes(options);
}
return await this._findAcrossAllTypes(options);
return await this._execute(
options.type,
'find',
{ options },
repository => repository.find(options)
);
}
async bulkGet(objects = []) {
@ -95,7 +94,7 @@ export class SecureSavedObjectsClient {
async _checkSavedObjectPrivileges(actions) {
try {
return await this._checkPrivileges(actions);
} catch(error) {
} catch (error) {
const { reason } = get(error, 'body.error', {});
throw this.errors.decorateGeneralError(error, reason);
}
@ -120,48 +119,4 @@ export class SecureSavedObjectsClient {
throw new Error('Unexpected result from hasPrivileges');
}
}
async _findAcrossAllTypes(options) {
const action = 'find';
// we have to filter for only their authorized types
const types = this._savedObjectTypes;
const typesToPrivilegesMap = new Map(types.map(type => [type, this._actions.getSavedObjectAction(type, action)]));
const { result, username, missing } = await this._checkSavedObjectPrivileges(Array.from(typesToPrivilegesMap.values()));
if (result === CHECK_PRIVILEGES_RESULT.LEGACY) {
return await this._callWithRequestRepository.find(options);
}
const authorizedTypes = Array.from(typesToPrivilegesMap.entries())
.filter(([ , privilege]) => !missing.includes(privilege))
.map(([type]) => type);
if (authorizedTypes.length === 0) {
this._auditLogger.savedObjectsAuthorizationFailure(
username,
action,
types,
missing,
{ options }
);
throw this.errors.decorateForbiddenError(new Error(`Not authorized to find saved_object`));
}
this._auditLogger.savedObjectsAuthorizationSuccess(username, action, authorizedTypes, { options });
return await this._internalRepository.find({
...options,
type: authorizedTypes
});
}
async _findWithTypes(options) {
return await this._execute(
options.type,
'find',
{ options },
repository => repository.find(options)
);
}
}

View file

@ -516,7 +516,7 @@ describe('#find', () => {
auditLogger: mockAuditLogger,
actions: mockActions,
});
const options = { type: [ type1, type2 ] };
const options = { type: [type1, type2] };
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
@ -598,9 +598,7 @@ describe('#find', () => {
});
describe('no type', () => {
test(`throws decorated GeneralError when hasPrivileges rejects promise`, async () => {
const type1 = 'foo';
const type2 = 'bar';
test(`throws error`, async () => {
const mockRepository = {};
const mockErrors = createMockErrors();
const mockCheckPrivileges = jest.fn().mockImplementation(async () => {
@ -613,161 +611,18 @@ describe('#find', () => {
repository: mockRepository,
checkPrivileges: mockCheckPrivileges,
auditLogger: mockAuditLogger,
savedObjectTypes: [type1, type2],
actions: mockActions,
});
await expect(client.find()).rejects.toThrowError(mockErrors.generalError);
expect(mockCheckPrivileges).toHaveBeenCalledWith([
mockActions.getSavedObjectAction(type1, 'find'),
mockActions.getSavedObjectAction(type2, 'find'),
mockActions.getSavedObjectAction(undefined, 'find'),
]);
expect(mockErrors.decorateGeneralError).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
});
test(`throws decorated ForbiddenError when unauthorized`, async () => {
const type = 'foo';
const username = Symbol();
const mockRepository = {};
const mockErrors = createMockErrors();
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
username,
missing: [
privileges[0]
],
}));
const mockAuditLogger = createMockAuditLogger();
const mockActions = createMockActions();
const client = new SecureSavedObjectsClient({
errors: mockErrors,
repository: mockRepository,
checkPrivileges: mockCheckPrivileges,
auditLogger: mockAuditLogger,
savedObjectTypes: [type],
actions: mockActions,
});
const options = Symbol();
await expect(client.find(options)).rejects.toThrowError(mockErrors.forbiddenError);
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]);
expect(mockErrors.decorateForbiddenError).toHaveBeenCalledTimes(1);
expect(mockAuditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith(
username,
'find',
[type],
[mockActions.getSavedObjectAction(type, 'find')],
{
options
}
);
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
});
test(`returns result of callWithRequestRepository.find when legacy`, async () => {
const type = 'foo';
const username = Symbol();
const returnValue = Symbol();
const mockRepository = {
find: jest.fn().mockReturnValue(returnValue)
};
const mockErrors = createMockErrors();
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
result: CHECK_PRIVILEGES_RESULT.LEGACY,
username,
missing: privileges,
}));
const mockAuditLogger = createMockAuditLogger();
const mockActions = createMockActions();
const client = new SecureSavedObjectsClient({
errors: mockErrors,
callWithRequestRepository: mockRepository,
checkPrivileges: mockCheckPrivileges,
auditLogger: mockAuditLogger,
savedObjectTypes: [type],
actions: mockActions,
});
const options = Symbol();
const result = await client.find(options);
expect(result).toBe(returnValue);
expect(mockCheckPrivileges).toHaveBeenCalledWith([mockActions.getSavedObjectAction(type, 'find')]);
expect(mockRepository.find).toHaveBeenCalledWith(options);
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).not.toHaveBeenCalled();
});
test(`specifies authorized types when calling repository.find()`, async () => {
const type1 = 'foo';
const type2 = 'bar';
const mockRepository = {
find: jest.fn(),
};
const mockErrors = createMockErrors();
const mockCheckPrivileges = jest.fn().mockImplementation(async privileges => ({
result: CHECK_PRIVILEGES_RESULT.UNAUTHORIZED,
missing: [
privileges[0]
]
}));
const mockAuditLogger = createMockAuditLogger();
const mockActions = createMockActions();
const client = new SecureSavedObjectsClient({
errors: mockErrors,
internalRepository: mockRepository,
checkPrivileges: mockCheckPrivileges,
auditLogger: mockAuditLogger,
savedObjectTypes: [type1, type2],
actions: mockActions,
});
await client.find();
expect(mockCheckPrivileges).toHaveBeenCalledWith([
mockActions.getSavedObjectAction(type1, 'find'),
mockActions.getSavedObjectAction(type2, 'find'),
]);
expect(mockRepository.find).toHaveBeenCalledWith(expect.objectContaining({
type: [type2]
}));
});
test(`returns result of repository.find`, async () => {
const type = 'foo';
const username = Symbol();
const returnValue = Symbol();
const mockRepository = {
find: jest.fn().mockReturnValue(returnValue)
};
const mockCheckPrivileges = jest.fn().mockImplementation(async () => ({
result: CHECK_PRIVILEGES_RESULT.AUTHORIZED,
username,
missing: [],
}));
const mockAuditLogger = createMockAuditLogger();
const client = new SecureSavedObjectsClient({
internalRepository: mockRepository,
checkPrivileges: mockCheckPrivileges,
auditLogger: mockAuditLogger,
savedObjectTypes: [type],
actions: createMockActions(),
});
const options = Symbol();
const result = await client.find(options);
expect(result).toBe(returnValue);
expect(mockRepository.find).toHaveBeenCalledWith({ type: [type] });
expect(mockAuditLogger.savedObjectsAuthorizationFailure).not.toHaveBeenCalled();
expect(mockAuditLogger.savedObjectsAuthorizationSuccess).toHaveBeenCalledWith(username, 'find', [type], {
options,
});
});
});
});

View file

@ -31,85 +31,15 @@ export default function ({ getService }) {
});
};
const expectResultsWithValidTypes = (resp) => {
const expectBadRequest = (resp) => {
expect(resp.body).to.eql({
page: 1,
per_page: 20,
total: 4,
saved_objects: [
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
updated_at: '2017-09-21T18:49:16.270Z',
version: 1,
attributes: resp.body.saved_objects[0].attributes
},
{
id: '7.0.0-alpha1',
type: 'config',
updated_at: '2017-09-21T18:49:16.302Z',
version: 1,
attributes: resp.body.saved_objects[1].attributes
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: 1,
attributes: resp.body.saved_objects[2].attributes
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: 1,
attributes: resp.body.saved_objects[3].attributes
},
]
});
};
const expectAllResultsIncludingInvalidTypes = (resp) => {
expect(resp.body).to.eql({
page: 1,
per_page: 20,
total: 5,
saved_objects: [
{
id: '91200a00-9efd-11e7-acb3-3dab96693fab',
type: 'index-pattern',
updated_at: '2017-09-21T18:49:16.270Z',
version: 1,
attributes: resp.body.saved_objects[0].attributes
},
{
id: '7.0.0-alpha1',
type: 'config',
updated_at: '2017-09-21T18:49:16.302Z',
version: 1,
attributes: resp.body.saved_objects[1].attributes
},
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: 1,
attributes: resp.body.saved_objects[2].attributes
},
{
id: 'be3733a0-9efe-11e7-acb3-3dab96693fab',
type: 'dashboard',
updated_at: '2017-09-21T18:57:40.826Z',
version: 1,
attributes: resp.body.saved_objects[3].attributes
},
{
id: 'visualization:dd7caf20-9efd-11e7-acb3-3dab96693faa',
type: 'not-a-visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: 1
},
]
error: 'Bad Request',
message: 'child "type" fails because ["type" is required]',
statusCode: 400,
validation: {
keys: ['type'],
source: 'query'
}
});
};
@ -130,14 +60,6 @@ export default function ({ getService }) {
});
};
const expectForbiddenCantFindAnyTypes = resp => {
expect(resp.body).to.eql({
statusCode: 403,
error: 'Forbidden',
message: `Not authorized to find saved_object`
});
};
const findTest = (description, { auth, tests }) => {
describe(description, () => {
before(() => esArchiver.load('saved_objects/basic'));
@ -221,8 +143,8 @@ export default function ({ getService }) {
},
noType: {
description: `forbidded can't find any types`,
statusCode: 403,
response: expectForbiddenCantFindAnyTypes,
statusCode: 400,
response: expectBadRequest,
}
}
});
@ -255,8 +177,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
statusCode: 400,
response: expectBadRequest,
},
},
});
@ -289,8 +211,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectAllResultsIncludingInvalidTypes,
statusCode: 400,
response: expectBadRequest,
},
},
});
@ -323,8 +245,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectAllResultsIncludingInvalidTypes,
statusCode: 400,
response: expectBadRequest,
},
}
});
@ -357,8 +279,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
statusCode: 400,
response: expectBadRequest,
},
},
});
@ -391,8 +313,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
statusCode: 400,
response: expectBadRequest,
},
}
});
@ -425,8 +347,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
statusCode: 400,
response: expectBadRequest,
},
},
});
@ -459,8 +381,8 @@ export default function ({ getService }) {
},
noType: {
description: 'all objects',
statusCode: 200,
response: expectResultsWithValidTypes,
statusCode: 400,
response: expectBadRequest,
},
}
});