Skip to content

Commit

Permalink
[App Search] Add new encodePathParams helper (fixes unencoded documen…
Browse files Browse the repository at this point in the history
…t IDs) (#88648)

* Add encodePathParams helper to EnterpriseSearchRequestHandler

This helper accomplishes two things:

- Fixes 404s from the Enterprise Search server for user-generated IDs with special characters (e.g. ? char)

- Allows us to simplify some of our createRequest calls - no longer will we have to grab request.params to create paths, this helper will do so for us

* Update AS document route to use new helper

- This was the primary view/API I was testing to fix this bug

* Update remaining AS routes to use new helper

- shorter, saves us a few lines
+ remove unnecessary payload: params that doesn't actually validate params

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
Constance and kibanamachine authored Jan 20, 2021
1 parent 8b1a228 commit f4f6cb6
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,40 @@ describe('EnterpriseSearchRequestHandler', () => {
);
});

it('correctly encodes paths and query string parameters', async () => {
it('correctly encodes query string parameters', async () => {
const requestHandler = enterpriseSearchRequestHandler.createRequest({
path: '/api/some example',
path: '/api/example',
});
await makeAPICall(requestHandler, { query: { 'page[current]': 1 } });

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/api/some%20example?page%5Bcurrent%5D=1'
'http://localhost:3002/api/example?page%5Bcurrent%5D=1'
);
});

describe('encodePathParams', () => {
it('correctly replaces :pathVariables with request.params', async () => {
const requestHandler = enterpriseSearchRequestHandler.createRequest({
path: '/api/examples/:example/some/:id',
});
await makeAPICall(requestHandler, { params: { example: 'hello', id: 'world' } });

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/api/examples/hello/some/world'
);
});

it('correctly encodes path params as URI components', async () => {
const requestHandler = enterpriseSearchRequestHandler.createRequest({
path: '/api/examples/:example',
});
await makeAPICall(requestHandler, { params: { example: 'hello#@/$%^/&[]{}/";world' } });

EnterpriseSearchAPI.shouldHaveBeenCalledWith(
'http://localhost:3002/api/examples/hello%23%40%2F%24%25%5E%2F%26%5B%5D%7B%7D%2F%22%3Bworld'
);
});
});
});

describe('response passing', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ export class EnterpriseSearchRequestHandler {
) => {
try {
// Set up API URL
const encodedPath = this.encodePathParams(path, request.params as Record<string, string>);
const queryParams = { ...(request.query as object), ...params };
const queryString = !this.isEmptyObj(queryParams)
? `?${querystring.stringify(queryParams)}`
: '';
const url = encodeURI(this.enterpriseSearchUrl + path) + queryString;
const url = encodeURI(this.enterpriseSearchUrl) + encodedPath + queryString;

// Set up API options
const { method } = request.route;
Expand Down Expand Up @@ -126,6 +127,36 @@ export class EnterpriseSearchRequestHandler {
};
}

/**
* This path helper is similar to React Router's generatePath, but much simpler &
* does not use regexes. It enables us to pass a static '/foo/:bar/baz' string to
* createRequest({ path }) and have :bar be automatically replaced by the value of
* request.params.bar.
* It also (very importantly) wraps all URL request params with encodeURIComponent(),
* which is an extra layer of encoding required by the Enterprise Search server in
* order to correctly & safely parse user-generated IDs with special characters in
* their names - just encodeURI alone won't work.
*/
encodePathParams(path: string, params: Record<string, string>) {
const hasParams = path.includes(':');
if (!hasParams) {
return path;
} else {
return path
.split('/')
.map((pathPart) => {
const isParam = pathPart.startsWith(':');
if (!isParam) {
return pathPart;
} else {
const pathParam = pathPart.replace(':', '');
return encodeURIComponent(params[pathParam]);
}
})
.join('/');
}
}

/**
* Attempt to grab a usable error body from Enterprise Search - this isn't
* always possible because some of our internal endpoints send back blank
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@ describe('analytics routes', () => {
});

it('creates a request handler', () => {
mockRouter.callRoute({
params: { engineName: 'some-engine' },
});

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/analytics/queries',
path: '/as/engines/:engineName/analytics/queries',
});
});

Expand Down Expand Up @@ -84,12 +80,8 @@ describe('analytics routes', () => {
});

it('creates a request handler', () => {
mockRouter.callRoute({
params: { engineName: 'some-engine', query: 'some-query' },
});

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/analytics/query/some-query',
path: '/as/engines/:engineName/analytics/query/:query',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,9 @@ export function registerAnalyticsRoutes({
query: schema.object(queriesSchema),
},
},
async (context, request, response) => {
const { engineName } = request.params;

return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${engineName}/analytics/queries`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: '/as/engines/:engineName/analytics/queries',
})
);

router.get(
Expand All @@ -52,12 +48,8 @@ export function registerAnalyticsRoutes({
query: schema.object(querySchema),
},
},
async (context, request, response) => {
const { engineName, query } = request.params;

return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${engineName}/analytics/query/${query}`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: '/as/engines/:engineName/analytics/query/:query',
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,8 @@ describe('credentials routes', () => {
});

it('creates a request to enterprise search', () => {
const mockRequest = {
params: {
name: 'abc123',
},
};

mockRouter.callRoute(mockRequest);

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/credentials/abc123',
path: '/as/credentials/:name',
});
});

Expand Down Expand Up @@ -311,7 +303,6 @@ describe('credentials routes', () => {
mockRouter = new MockRouter({
method: 'delete',
path: '/api/app_search/credentials/{name}',
payload: 'params',
});

registerCredentialsRoutes({
Expand All @@ -321,16 +312,8 @@ describe('credentials routes', () => {
});

it('creates a request to enterprise search', () => {
const mockRequest = {
params: {
name: 'abc123',
},
};

mockRouter.callRoute(mockRequest);

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/credentials/abc123',
path: '/as/credentials/:name',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,9 @@ export function registerCredentialsRoutes({
body: tokenSchema,
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/credentials/${request.params.name}`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: '/as/credentials/:name',
})
);
router.delete(
{
Expand All @@ -96,10 +94,8 @@ export function registerCredentialsRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/credentials/${request.params.name}`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: '/as/credentials/:name',
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,8 @@ describe('documents routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({
params: { engineName: 'some-engine' },
body: { documents: [{ foo: 'bar' }] },
});

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/documents/new',
path: '/as/engines/:engineName/documents/new',
});
});

Expand Down Expand Up @@ -79,10 +74,8 @@ describe('document routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } });

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/documents/1',
path: '/as/engines/:engineName/documents/:documentId',
});
});
});
Expand All @@ -104,10 +97,8 @@ describe('document routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({ params: { engineName: 'some-engine', documentId: '1' } });

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/documents/1',
path: '/as/engines/:engineName/documents/:documentId',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,9 @@ export function registerDocumentsRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${request.params.engineName}/documents/new`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/:engineName/documents/new`,
})
);
}

Expand All @@ -46,11 +44,9 @@ export function registerDocumentRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/:engineName/documents/:documentId`,
})
);
router.delete(
{
Expand All @@ -62,10 +58,8 @@ export function registerDocumentRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${request.params.engineName}/documents/${request.params.documentId}`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/:engineName/documents/:documentId`,
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('engine routes', () => {

describe('hasValidData', () => {
it('should correctly validate that the response has data', () => {
mockRequestHandler.createRequest.mockClear();
const response = {
meta: {
page: {
Expand All @@ -73,6 +74,7 @@ describe('engine routes', () => {
});

it('should correctly validate that a response does not have data', () => {
mockRequestHandler.createRequest.mockClear();
const response = {};

mockRouter.callRoute(mockRequest);
Expand Down Expand Up @@ -125,10 +127,8 @@ describe('engine routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({ params: { name: 'some-engine' } });

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/details',
path: '/as/engines/:name/details',
});
});
});
Expand All @@ -150,10 +150,8 @@ describe('engine routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({ params: { name: 'some-engine' } });

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/some-engine/overview_metrics',
path: '/as/engines/:name/overview_metrics',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,9 @@ export function registerEnginesRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${request.params.name}/details`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/:name/details`,
})
);
router.get(
{
Expand All @@ -69,10 +67,8 @@ export function registerEnginesRoutes({
}),
},
},
async (context, request, response) => {
return enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/${request.params.name}/overview_metrics`,
})(context, request, response);
}
enterpriseSearchRequestHandler.createRequest({
path: `/as/engines/:name/overview_metrics`,
})
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ describe('log settings routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({});

expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/log_settings',
});
Expand All @@ -52,7 +50,6 @@ describe('log settings routes', () => {
});

it('creates a request to enterprise search', () => {
mockRouter.callRoute({});
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/log_settings',
});
Expand Down
Loading

0 comments on commit f4f6cb6

Please sign in to comment.