Unverified Commit 29393825 authored by Tamika Tannis's avatar Tamika Tannis Committed by GitHub

Improve granularity of logging search actions (#396)

* WIP: One approach

* Second approach

* Fix a few errors; Add tests

* Fix test

* Code cleanup

* Change value for when user selects inline result & searchAll is needed

* Update SearchType enum

* Use snake_case for consistency in backend until we have some auto-convert
parent 5aaec51b
...@@ -11,8 +11,7 @@ from flask.blueprints import Blueprint ...@@ -11,8 +11,7 @@ from flask.blueprints import Blueprint
from amundsen_application.log.action_log import action_logging from amundsen_application.log.action_log import action_logging
from amundsen_application.api.utils.request_utils import get_query_param, request_search from amundsen_application.api.utils.request_utils import get_query_param, request_search
from amundsen_application.api.utils.response_utils import create_error_response from amundsen_application.api.utils.search_utils import generate_query_json, map_table_result
from amundsen_application.api.utils.search_utils import generate_query_json, map_table_result, valid_search_fields
from amundsen_application.models.user import load_user, dump_user from amundsen_application.models.user import load_user, dump_user
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
...@@ -21,151 +20,53 @@ REQUEST_SESSION_TIMEOUT_SEC = 3 ...@@ -21,151 +20,53 @@ REQUEST_SESSION_TIMEOUT_SEC = 3
search_blueprint = Blueprint('search', __name__, url_prefix='/api/search/v0') search_blueprint = Blueprint('search', __name__, url_prefix='/api/search/v0')
SEARCH_ENDPOINT = '/search' SEARCH_TABLE_ENDPOINT = '/search_table'
SEARCH_USER_ENDPOINT = '/search_user' SEARCH_USER_ENDPOINT = '/search_user'
# TODO: To be deprecated pending full community support @search_blueprint.route('/table', methods=['POST'])
def _validate_search_term(*, search_term: str, page_index: int) -> Optional[Response]:
error_payload = {
'results': [],
'search_term': search_term,
'total_results': 0,
'page_index': page_index,
}
# use colon means user would like to search on specific fields
if search_term.count(':') > 1:
message = 'Encountered error: Search field should not be more than 1'
return create_error_response(message=message, payload=error_payload, status_code=HTTPStatus.BAD_REQUEST)
if search_term.count(':') == 1:
field_key = search_term.split(' ')[0].split(':')[0]
if field_key not in valid_search_fields:
message = 'Encountered error: Search field is invalid'
return create_error_response(message=message, payload=error_payload, status_code=HTTPStatus.BAD_REQUEST)
return None
# TODO: To be deprecated pending full community support
@search_blueprint.route('/table', methods=['GET'])
def search_table() -> Response: def search_table() -> Response:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter')
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter')
error_response = _validate_search_term(search_term=search_term, page_index=int(page_index))
if error_response is not None:
return error_response
results_dict = _search_table(search_term=search_term, page_index=page_index)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
# TODO: To be deprecated pending full community support
@action_logging
def _search_table(*, search_term: str, page_index: int) -> Dict[str, Any]:
""" """
call the search service endpoint and return matching results Parse the request arguments and call the helper method to execute a table search
:return: a json output containing search results array as 'results' :return: a Response created with the results from the helper method
Schema Defined Here:
https://github.com/lyft/amundsensearchlibrary/blob/master/search_service/api/search.py
TODO: Define an interface for envoy_client
""" """
tables = {
'page_index': int(page_index),
'results': [],
'total_results': 0,
}
results_dict = {
'search_term': search_term,
'msg': '',
'tables': tables,
}
try: try:
if ':' in search_term: request_json = request.get_json()
url = _create_url_with_field(search_term=search_term,
page_index=page_index)
else:
url = '{0}?query_term={1}&page_index={2}'.format(app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
search_term,
page_index)
response = request_search(url=url) search_term = get_query_param(request_json, 'term', '"term" parameter expected in request data')
status_code = response.status_code page_index = get_query_param(request_json, 'pageIndex', '"pageIndex" parameter expected in request data')
if status_code == HTTPStatus.OK: search_type = request_json.get('searchType')
results_dict['msg'] = 'Success'
results = response.json().get('results')
tables['results'] = [map_table_result(result) for result in results]
tables['total_results'] = response.json().get('total_results')
else:
message = 'Encountered error: Search request failed'
results_dict['msg'] = message
logging.error(message)
results_dict['status_code'] = status_code filters = request_json.get('filters', {})
return results_dict
results_dict = _search_table(filters=filters,
search_term=search_term,
page_index=page_index,
search_type=search_type)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
except Exception as e: except Exception as e:
message = 'Encountered exception: ' + str(e) message = 'Encountered exception: ' + str(e)
results_dict['msg'] = message
logging.exception(message) logging.exception(message)
return results_dict return make_response(jsonify(results_dict), HTTPStatus.INTERNAL_SERVER_ERROR)
# TODO: To be deprecated pending full community support @action_logging
def _create_url_with_field(*, search_term: str, page_index: int) -> str: def _search_table(*, search_term: str, page_index: int, filters: Dict, search_type: str) -> Dict[str, Any]:
"""
Construct a url by searching specific field.
E.g if we use search tag:hive test_table, search service will first
filter all the results that
don't have tag hive; then it uses test_table as query term to search /
rank all the documents.
We currently allow max 1 field.
todo: allow search multiple fields(e.g tag:hive & schema:default test_table)
:param search_term:
:param page_index:
:return:
"""
# example search_term: tag:tag_name search_term search_term2
fields = search_term.split(' ')
search_field = fields[0].split(':')
field_key = search_field[0]
# dedup tag to all lower case
field_val = search_field[1].lower()
search_term = ' '.join(fields[1:])
url = '{0}/field/{1}/field_val/{2}' \
'?page_index={3}'.format(app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
field_key,
field_val,
page_index)
if search_term:
url += '&query_term={0}'.format(search_term)
return url
@search_blueprint.route('/table_qs', methods=['POST'])
def search_table_query_string() -> Response:
"""
TODO (ttannis): Update this docstring after amundsensearch documentation is merged
Calls the search service to execute a search. The request data is transformed
to the json payload defined [link]
""" """
request_json = request.get_json() Call the search service endpoint and return matching results
Search service logic defined here:
search_term = get_query_param(request_json, 'term', '"term" parameter expected in request data') https://github.com/lyft/amundsensearchlibrary/blob/master/search_service/api/table.py
page_index = get_query_param(request_json, 'pageIndex', '"pageIndex" parameter expected in request data')
filters = request_json.get('filters', {})
:return: a json output containing search results array as 'results'
"""
# Default results # Default results
tables = { tables = {
'page_index': int(page_index), 'page_index': int(page_index),
'results': [], 'results': [],
'total_results': 0, 'total_results': 0,
} }
results_dict = { results_dict = {
'search_term': search_term, 'search_term': search_term,
'msg': '', 'msg': '',
...@@ -178,11 +79,10 @@ def search_table_query_string() -> Response: ...@@ -178,11 +79,10 @@ def search_table_query_string() -> Response:
message = 'Encountered exception generating query json: ' + str(e) message = 'Encountered exception generating query json: ' + str(e)
results_dict['msg'] = message results_dict['msg'] = message
logging.exception(message) logging.exception(message)
return make_response(jsonify(results_dict), HTTPStatus.INTERNAL_SERVER_ERROR) return results_dict
try: try:
# TODO (ttannis): Change actual endpoint name after amundsensearch PR is merged url = app.config['SEARCHSERVICE_BASE'] + SEARCH_TABLE_ENDPOINT
url = app.config['SEARCHSERVICE_BASE'] + '/search_table'
response = request_search(url=url, response = request_search(url=url,
headers={'Content-Type': 'application/json'}, headers={'Content-Type': 'application/json'},
method='POST', method='POST',
...@@ -199,32 +99,42 @@ def search_table_query_string() -> Response: ...@@ -199,32 +99,42 @@ def search_table_query_string() -> Response:
logging.error(message) logging.error(message)
results_dict['status_code'] = status_code results_dict['status_code'] = status_code
return make_response(jsonify(results_dict), status_code) return results_dict
except Exception as e: except Exception as e:
message = 'Encountered exception: ' + str(e) message = 'Encountered exception: ' + str(e)
results_dict['msg'] = message results_dict['msg'] = message
logging.exception(message) logging.exception(message)
return make_response(jsonify(results_dict), HTTPStatus.INTERNAL_SERVER_ERROR) return results_dict
@search_blueprint.route('/user', methods=['GET']) @search_blueprint.route('/user', methods=['GET'])
def search_user() -> Response: def search_user() -> Response:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter') """
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter') Parse the request arguments and call the helper method to execute a user search
results_dict = _search_user(search_term=search_term, page_index=page_index) :return: a Response created with the results from the helper method
"""
try:
search_term = get_query_param(request.args, 'query', 'Endpoint takes a "query" parameter')
page_index = get_query_param(request.args, 'page_index', 'Endpoint takes a "page_index" parameter')
search_type = request.args.get('search_type')
results_dict = _search_user(search_term=search_term, page_index=page_index, search_type=search_type)
return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR)) return make_response(jsonify(results_dict), results_dict.get('status_code', HTTPStatus.INTERNAL_SERVER_ERROR))
except Exception as e:
message = 'Encountered exception: ' + str(e)
logging.exception(message)
return make_response(jsonify(results_dict), HTTPStatus.INTERNAL_SERVER_ERROR)
@action_logging @action_logging
def _search_user(*, search_term: str, page_index: int) -> Dict[str, Any]: def _search_user(*, search_term: str, page_index: int, search_type: str) -> Dict[str, Any]:
""" """
call the search service endpoint and return matching results Call the search service endpoint and return matching results
:return: a json output containing search results array as 'results' Search service logic defined here:
Schema Defined Here:
https://github.com/lyft/amundsensearchlibrary/blob/master/search_service/api/user.py https://github.com/lyft/amundsensearchlibrary/blob/master/search_service/api/user.py
TODO: Define an interface for envoy_client
:return: a json output containing search results array as 'results'
""" """
def _map_user_result(result: Dict) -> Dict: def _map_user_result(result: Dict) -> Dict:
...@@ -268,10 +178,11 @@ def _search_user(*, search_term: str, page_index: int) -> Dict[str, Any]: ...@@ -268,10 +178,11 @@ def _search_user(*, search_term: str, page_index: int) -> Dict[str, Any]:
except Exception as e: except Exception as e:
message = 'Encountered exception: ' + str(e) message = 'Encountered exception: ' + str(e)
results_dict['msg'] = message results_dict['msg'] = message
results_dict['status_code'] = HTTPStatus.INTERNAL_SERVER_ERROR
logging.exception(message) logging.exception(message)
return results_dict return results_dict
# TODO - Implement # TODO - Implement
def _search_dashboard(*, search_term: str, page_index: int) -> Dict[str, Any]: def _search_dashboard(*, search_term: str, page_index: int, filters: Dict, search_type: str) -> Dict[str, Any]:
return {} return {}
...@@ -23,11 +23,11 @@ def map_table_result(result: Dict) -> Dict: ...@@ -23,11 +23,11 @@ def map_table_result(result: Dict) -> Dict:
} }
def generate_query_json(*, filters: Dict = {}, page_index: str, search_term: str) -> Dict: def generate_query_json(*, filters: Dict = {}, page_index: int, search_term: str) -> Dict:
""" """
Transforms the given paramaters to the query json for the search service according to Transforms the given paramaters to the query json for the search service according to
the api defined at: the api defined at:
TODO (ttannis): Add link when amundsensearch PR is complete https://github.com/lyft/amundsensearchlibrary/blob/master/search_service/api/swagger_doc/table/search_table_filter.yml
""" """
# Generate the filter payload # Generate the filter payload
filter_payload = {} filter_payload = {}
......
...@@ -4,7 +4,7 @@ import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'd ...@@ -4,7 +4,7 @@ import { DashboardSearchResults, TableSearchResults, UserSearchResults } from 'd
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import { ResourceType } from 'interfaces'; import { ResourceType, SearchType } from 'interfaces';
import * as API from '../v0'; import * as API from '../v0';
...@@ -44,7 +44,7 @@ describe('searchResource', () => { ...@@ -44,7 +44,7 @@ describe('searchResource', () => {
const resourceType = ResourceType.dashboard; const resourceType = ResourceType.dashboard;
const term = 'test'; const term = 'test';
expect.assertions(3); expect.assertions(3);
await API.searchResource(pageIndex, resourceType, term).then(results => { await API.searchResource(pageIndex, resourceType, term, undefined, SearchType.FILTER).then(results => {
expect(results).toEqual({}); expect(results).toEqual({});
}); });
expect(axiosMockGet).not.toHaveBeenCalled(); expect(axiosMockGet).not.toHaveBeenCalled();
...@@ -59,7 +59,7 @@ describe('searchResource', () => { ...@@ -59,7 +59,7 @@ describe('searchResource', () => {
const resourceType = ResourceType.user; const resourceType = ResourceType.user;
const term = 'test'; const term = 'test';
expect.assertions(3); expect.assertions(3);
await API.searchResource(pageIndex, resourceType, term).then(results => { await API.searchResource(pageIndex, resourceType, term, undefined, SearchType.FILTER).then(results => {
expect(results).toEqual({}); expect(results).toEqual({});
}); });
expect(axiosMockGet).not.toHaveBeenCalled(); expect(axiosMockGet).not.toHaveBeenCalled();
...@@ -74,14 +74,15 @@ describe('searchResource', () => { ...@@ -74,14 +74,15 @@ describe('searchResource', () => {
const pageIndex = 0; const pageIndex = 0;
const resourceType = ResourceType.user; const resourceType = ResourceType.user;
const term = 'test'; const term = 'test';
await API.searchResource(pageIndex, resourceType, term); const searchType = SearchType.SUBMIT_TERM;
expect(axiosMockGet).toHaveBeenCalledWith(`${API.BASE_URL}/${resourceType}?query=${term}&page_index=${pageIndex}`); await API.searchResource(pageIndex, resourceType, term, undefined, searchType);
expect(axiosMockGet).toHaveBeenCalledWith(`${API.BASE_URL}/${resourceType}?query=${term}&page_index=${pageIndex}&search_type=${searchType}`);
expect(axiosMockPost).not.toHaveBeenCalled(); expect(axiosMockPost).not.toHaveBeenCalled();
}); });
it('calls searchResourceHelper with api call response', async () => { it('calls searchResourceHelper with api call response', async () => {
const searchResourceHelperSpy = jest.spyOn(API, 'searchResourceHelper'); const searchResourceHelperSpy = jest.spyOn(API, 'searchResourceHelper');
await API.searchResource(0, ResourceType.user, 'test'); await API.searchResource(0, ResourceType.user, 'test', undefined, SearchType.FILTER);
expect(searchResourceHelperSpy).toHaveBeenCalledWith(mockSearchResponse); expect(searchResourceHelperSpy).toHaveBeenCalledWith(mockSearchResponse);
}); });
}) })
...@@ -94,18 +95,20 @@ describe('searchResource', () => { ...@@ -94,18 +95,20 @@ describe('searchResource', () => {
const resourceType = ResourceType.table; const resourceType = ResourceType.table;
const term = 'test'; const term = 'test';
const filters = { 'schema': 'schema_name' } const filters = { 'schema': 'schema_name' }
await API.searchResource(pageIndex, resourceType, term, filters); const searchType = SearchType.SUBMIT_TERM;
await API.searchResource(pageIndex, resourceType, term, filters, searchType);
expect(axiosMockGet).not.toHaveBeenCalled(); expect(axiosMockGet).not.toHaveBeenCalled();
expect(axiosMockPost).toHaveBeenCalledWith(`${API.BASE_URL}/${resourceType}_qs`, { expect(axiosMockPost).toHaveBeenCalledWith(`${API.BASE_URL}/${resourceType}`, {
filters, filters,
pageIndex, pageIndex,
term, term,
searchType,
}); });
}); });
it('calls searchResourceHelper with api call response', async () => { it('calls searchResourceHelper with api call response', async () => {
const searchResourceHelperSpy = jest.spyOn(API, 'searchResourceHelper'); const searchResourceHelperSpy = jest.spyOn(API, 'searchResourceHelper');
await API.searchResource(0, ResourceType.table, 'test', { 'schema': 'schema_name' }); await API.searchResource(0, ResourceType.table, 'test', { 'schema': 'schema_name' }, SearchType.FILTER);
expect(searchResourceHelperSpy).toHaveBeenCalledWith(mockSearchResponse); expect(searchResourceHelperSpy).toHaveBeenCalledWith(mockSearchResponse);
}); });
}) })
......
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { indexUsersEnabled } from 'config/config-utils'; import { indexUsersEnabled } from 'config/config-utils';
import { ResourceType } from 'interfaces'; import { ResourceType, SearchType } from 'interfaces';
import { DashboardSearchResults, TableSearchResults, UserSearchResults } from '../types'; import { DashboardSearchResults, TableSearchResults, UserSearchResults } from '../types';
...@@ -29,7 +29,7 @@ export const searchResourceHelper = (response: AxiosResponse<SearchAPI>) => { ...@@ -29,7 +29,7 @@ export const searchResourceHelper = (response: AxiosResponse<SearchAPI>) => {
return ret; return ret;
}; };
export function searchResource(pageIndex: number, resource: ResourceType, term: string, filters: ResourceFilterReducerState = {}) { export function searchResource(pageIndex: number, resource: ResourceType, term: string, filters: ResourceFilterReducerState = {}, searchType: SearchType) {
if (resource === ResourceType.dashboard || if (resource === ResourceType.dashboard ||
(resource === ResourceType.user && !indexUsersEnabled())) { (resource === ResourceType.user && !indexUsersEnabled())) {
return Promise.resolve({}); return Promise.resolve({});
...@@ -37,12 +37,13 @@ export function searchResource(pageIndex: number, resource: ResourceType, term: ...@@ -37,12 +37,13 @@ export function searchResource(pageIndex: number, resource: ResourceType, term:
/* Note: This logic must exist until query string endpoints are created for all resources */ /* Note: This logic must exist until query string endpoints are created for all resources */
if (resource === ResourceType.table) { if (resource === ResourceType.table) {
return axios.post(`${BASE_URL}/${resource}_qs`, { return axios.post(`${BASE_URL}/${resource}`, {
filters, filters,
pageIndex, pageIndex,
term, term,
searchType,
}).then(searchResourceHelper); }).then(searchResourceHelper);
} }
return axios.get(`${BASE_URL}/${resource}?query=${term}&page_index=${pageIndex}`) return axios.get(`${BASE_URL}/${resource}?query=${term}&page_index=${pageIndex}&search_type=${searchType}`)
.then(searchResourceHelper); .then(searchResourceHelper);
}; };
import { ResourceType } from 'interfaces'; import { ResourceType, SearchType} from 'interfaces';
import { Search as UrlSearch } from 'history'; import { Search as UrlSearch } from 'history';
...@@ -49,13 +49,14 @@ export interface SearchReducerState { ...@@ -49,13 +49,14 @@ export interface SearchReducerState {
}; };
/* ACTIONS */ /* ACTIONS */
export function searchAll(term: string, resource?: ResourceType, pageIndex?: number, useFilters: boolean = false): SearchAllRequest { export function searchAll(searchType: SearchType, term: string, resource?: ResourceType, pageIndex?: number, useFilters: boolean = false): SearchAllRequest {
return { return {
payload: { payload: {
resource, resource,
pageIndex, pageIndex,
term, term,
useFilters, useFilters,
searchType,
}, },
type: SearchAll.REQUEST, type: SearchAll.REQUEST,
}; };
...@@ -67,12 +68,13 @@ export function searchAllFailure(): SearchAllResponse { ...@@ -67,12 +68,13 @@ export function searchAllFailure(): SearchAllResponse {
return { type: SearchAll.FAILURE }; return { type: SearchAll.FAILURE };
}; };
export function searchResource(term: string, resource: ResourceType, pageIndex: number): SearchResourceRequest { export function searchResource(searchType: SearchType, term: string, resource: ResourceType, pageIndex: number): SearchResourceRequest {
return { return {
payload: { payload: {
pageIndex, pageIndex,
term, term,
resource, resource,
searchType
}, },
type: SearchResource.REQUEST, type: SearchResource.REQUEST,
}; };
......
...@@ -3,7 +3,7 @@ import { all, call, debounce, put, select, takeEvery, takeLatest } from 'redux-s ...@@ -3,7 +3,7 @@ import { all, call, debounce, put, select, takeEvery, takeLatest } from 'redux-s
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as qs from 'simple-query-string'; import * as qs from 'simple-query-string';
import { ResourceType } from 'interfaces/Resources'; import { ResourceType, SearchType } from 'interfaces';
import * as API from './api/v0'; import * as API from './api/v0';
...@@ -68,7 +68,7 @@ export function* filterWorker(): SagaIterator { ...@@ -68,7 +68,7 @@ export function* filterWorker(): SagaIterator {
const state = yield select(getSearchState); const state = yield select(getSearchState);
const { search_term, selectedTab, filters } = state; const { search_term, selectedTab, filters } = state;
const pageIndex = getPageIndex(state) const pageIndex = getPageIndex(state)
yield put(searchResource(search_term, selectedTab, pageIndex)); yield put(searchResource(SearchType.FILTER, search_term, selectedTab, pageIndex));
updateSearchUrl({ filters, resource: selectedTab, term: search_term, index: pageIndex }, true); updateSearchUrl({ filters, resource: selectedTab, term: search_term, index: pageIndex }, true);
}; };
...@@ -90,7 +90,7 @@ export function* filterWatcher2(): SagaIterator { ...@@ -90,7 +90,7 @@ export function* filterWatcher2(): SagaIterator {
export function* filterWorker2(action: any): SagaIterator { export function* filterWorker2(action: any): SagaIterator {
const state = yield select(getSearchState); const state = yield select(getSearchState);
const { pageIndex = 0, resourceType, term = '' } = action.payload; const { pageIndex = 0, resourceType, term = '' } = action.payload;
yield put(searchResource(term, resourceType, pageIndex)); yield put(searchResource(SearchType.FILTER, term, resourceType, pageIndex));
updateSearchUrl({ term, filters: state.filters, resource: resourceType, index: pageIndex }, false); updateSearchUrl({ term, filters: state.filters, resource: resourceType, index: pageIndex }, false);
}; };
...@@ -98,8 +98,8 @@ export function* inlineSearchWorker(action: InlineSearchRequest): SagaIterator { ...@@ -98,8 +98,8 @@ export function* inlineSearchWorker(action: InlineSearchRequest): SagaIterator {
const { term } = action.payload; const { term } = action.payload;
try { try {
const [tableResponse, userResponse] = yield all([ const [tableResponse, userResponse] = yield all([
call(API.searchResource, 0, ResourceType.table, term), call(API.searchResource, 0, ResourceType.table, term, {}, SearchType.INLINE_SEARCH),
call(API.searchResource, 0, ResourceType.user, term), call(API.searchResource, 0, ResourceType.user, term, {}, SearchType.INLINE_SEARCH),
]); ]);
const inlineSearchResponse = { const inlineSearchResponse = {
tables: tableResponse.tables || initialInlineResultsState.tables, tables: tableResponse.tables || initialInlineResultsState.tables,
...@@ -124,7 +124,7 @@ export function* selectInlineResultWorker(action): SagaIterator { ...@@ -124,7 +124,7 @@ export function* selectInlineResultWorker(action): SagaIterator {
const state = yield select(); const state = yield select();
const { searchTerm, resourceType, updateUrl } = action.payload; const { searchTerm, resourceType, updateUrl } = action.payload;
if (state.search.inlineResults.isLoading) { if (state.search.inlineResults.isLoading) {
yield put(searchAll(searchTerm, resourceType, 0)) yield put(searchAll(SearchType.INLINE_SELECT, searchTerm, resourceType, 0, false))
updateSearchUrl({ term: searchTerm, filters: state.search.filters }); updateSearchUrl({ term: searchTerm, filters: state.search.filters });
} }
else { else {
...@@ -144,67 +144,10 @@ export function* selectInlineResultsWatcher(): SagaIterator { ...@@ -144,67 +144,10 @@ export function* selectInlineResultsWatcher(): SagaIterator {
yield takeEvery(InlineSearch.SELECT, selectInlineResultWorker); yield takeEvery(InlineSearch.SELECT, selectInlineResultWorker);
}; };
export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
let { resource } = action.payload;
const { pageIndex, term, useFilters } = action.payload;
if (!useFilters) {
yield put(clearAllFilters())
}
const state = yield select(getSearchState);
const tableIndex = resource === ResourceType.table ? pageIndex : 0;
const userIndex = resource === ResourceType.user ? pageIndex : 0;
const dashboardIndex = resource === ResourceType.dashboard ? pageIndex : 0;
try {
const [tableResponse, userResponse, dashboardResponse] = yield all([
call(API.searchResource, tableIndex, ResourceType.table, term, state.filters[ResourceType.table]),
call(API.searchResource, userIndex, ResourceType.user, term, state.filters[ResourceType.user]),
call(API.searchResource, dashboardIndex, ResourceType.dashboard, term, state.filters[ResourceType.dashboard]),
]);
const searchAllResponse = {
search_term: term,
selectedTab: resource,
tables: tableResponse.tables || initialState.tables,
users: userResponse.users || initialState.users,
dashboards: dashboardResponse.dashboards || initialState.dashboards,
isLoading: false,
};
if (resource === undefined) {
resource = autoSelectResource(searchAllResponse);
searchAllResponse.selectedTab = resource;
}
const index = getPageIndex(searchAllResponse);
yield put(searchAllSuccess(searchAllResponse));
updateSearchUrl({ term, resource, index, filters: state.filters }, true);
} catch (e) {
yield put(searchAllFailure());
}
};
export function* searchAllWatcher(): SagaIterator {
yield takeEvery(SearchAll.REQUEST, searchAllWorker);
};
export function* searchResourceWorker(action: SearchResourceRequest): SagaIterator {
const { pageIndex, resource, term } = action.payload;
const state = yield select(getSearchState);
try {
const searchResults = yield call(API.searchResource, pageIndex, resource, term, state.filters[resource]);
yield put(searchResourceSuccess(searchResults));
} catch (e) {
yield put(searchResourceFailure());
}
};
export function* searchResourceWatcher(): SagaIterator {
yield takeEvery(SearchResource.REQUEST, searchResourceWorker);
};
export function* submitSearchWorker(action: SubmitSearchRequest): SagaIterator { export function* submitSearchWorker(action: SubmitSearchRequest): SagaIterator {
const state = yield select(getSearchState); const state = yield select(getSearchState);
const { searchTerm, useFilters } = action.payload; const { searchTerm, useFilters } = action.payload;
yield put(searchAll(searchTerm, undefined, undefined, useFilters)); yield put(searchAll(SearchType.SUBMIT_TERM, searchTerm, undefined, undefined, useFilters));
updateSearchUrl({ term: searchTerm, filters: state.filters }); updateSearchUrl({ term: searchTerm, filters: state.filters });
}; };
export function* submitSearchWatcher(): SagaIterator { export function* submitSearchWatcher(): SagaIterator {
...@@ -230,7 +173,7 @@ export function* setResourceWatcher(): SagaIterator { ...@@ -230,7 +173,7 @@ export function* setResourceWatcher(): SagaIterator {
export function* setPageIndexWorker(action: SetPageIndexRequest): SagaIterator { export function* setPageIndexWorker(action: SetPageIndexRequest): SagaIterator {
const { pageIndex, updateUrl } = action.payload; const { pageIndex, updateUrl } = action.payload;
const state = yield select(getSearchState); const state = yield select(getSearchState);
yield put(searchResource(state.search_term, state.selectedTab, pageIndex)); yield put(searchResource(SearchType.PAGINATION, state.search_term, state.selectedTab, pageIndex));
if (updateUrl) { if (updateUrl) {
updateSearchUrl({ updateSearchUrl({
...@@ -249,7 +192,7 @@ export function* clearSearchWorker(action: ClearSearchRequest): SagaIterator { ...@@ -249,7 +192,7 @@ export function* clearSearchWorker(action: ClearSearchRequest): SagaIterator {
/* If there was a previous search term, search each resource using filters */ /* If there was a previous search term, search each resource using filters */
const state = yield select(getSearchState); const state = yield select(getSearchState);
if (!!state.search_term) { if (!!state.search_term) {
yield put(searchAll('', undefined, undefined, true)); yield put(searchAll(SearchType.CLEAR_TERM, '', undefined, undefined, true));
} }
}; };
export function* clearSearchWatcher(): SagaIterator { export function* clearSearchWatcher(): SagaIterator {
...@@ -264,7 +207,7 @@ export function* urlDidUpdateWorker(action: UrlDidUpdateRequest): SagaIterator { ...@@ -264,7 +207,7 @@ export function* urlDidUpdateWorker(action: UrlDidUpdateRequest): SagaIterator {
const state = yield select(getSearchState); const state = yield select(getSearchState);
if (!!term && state.search_term !== term) { if (!!term && state.search_term !== term) {
yield put(searchAll(term, resource, parsedIndex)); yield put(searchAll(SearchType.LOAD_URL, term, resource, parsedIndex));
} else if (!!resource) { } else if (!!resource) {
if (resource !== state.selectedTab) { if (resource !== state.selectedTab) {
yield put(setResource(resource, false)) yield put(setResource(resource, false))
...@@ -302,3 +245,66 @@ export function* loadPreviousSearchWorker(action: LoadPreviousSearchRequest): Sa ...@@ -302,3 +245,66 @@ export function* loadPreviousSearchWorker(action: LoadPreviousSearchRequest): Sa
export function* loadPreviousSearchWatcher(): SagaIterator { export function* loadPreviousSearchWatcher(): SagaIterator {
yield takeEvery(LoadPreviousSearch.REQUEST, loadPreviousSearchWorker); yield takeEvery(LoadPreviousSearch.REQUEST, loadPreviousSearchWorker);
}; };
//////////////////////////////////////////////////////////////////////////////
// API/END SAGAS
// These sagas directly trigger axios search requests.
// The actions that trigger them should only be fired by other sagas,
// and these sagas should be considered the "end" of any saga chain.
//////////////////////////////////////////////////////////////////////////////
export function* searchResourceWorker(action: SearchResourceRequest): SagaIterator {
const { pageIndex, resource, term, searchType } = action.payload;
const state = yield select(getSearchState);
try {
const searchResults = yield call(API.searchResource, pageIndex, resource, term, state.filters[resource], searchType);
yield put(searchResourceSuccess(searchResults));
} catch (e) {
yield put(searchResourceFailure());
}
};
export function* searchResourceWatcher(): SagaIterator {
yield takeEvery(SearchResource.REQUEST, searchResourceWorker);
};
export function* searchAllWorker(action: SearchAllRequest): SagaIterator {
let { resource } = action.payload;
const { pageIndex, term, useFilters, searchType } = action.payload;
if (!useFilters) {
yield put(clearAllFilters())
}
const state = yield select(getSearchState);
const tableIndex = resource === ResourceType.table ? pageIndex : 0;
const userIndex = resource === ResourceType.user ? pageIndex : 0;
const dashboardIndex = resource === ResourceType.dashboard ? pageIndex : 0;
try {
const [tableResponse, userResponse, dashboardResponse] = yield all([
call(API.searchResource, tableIndex, ResourceType.table, term, state.filters[ResourceType.table], searchType),
call(API.searchResource, userIndex, ResourceType.user, term, state.filters[ResourceType.user], searchType),
call(API.searchResource, dashboardIndex, ResourceType.dashboard, term, state.filters[ResourceType.dashboard], searchType),
]);
const searchAllResponse = {
search_term: term,
selectedTab: resource,
tables: tableResponse.tables || initialState.tables,
users: userResponse.users || initialState.users,
dashboards: dashboardResponse.dashboards || initialState.dashboards,
isLoading: false,
};
if (resource === undefined) {
resource = autoSelectResource(searchAllResponse);
searchAllResponse.selectedTab = resource;
}
const index = getPageIndex(searchAllResponse);
yield put(searchAllSuccess(searchAllResponse));
updateSearchUrl({ term, resource, index, filters: state.filters }, true);
} catch (e) {
yield put(searchAllFailure());
}
};
export function* searchAllWatcher(): SagaIterator {
yield takeEvery(SearchAll.REQUEST, searchAllWorker);
};
import { testSaga } from 'redux-saga-test-plan'; import { testSaga } from 'redux-saga-test-plan';
import { debounce } from 'redux-saga/effects'; import { debounce } from 'redux-saga/effects';
import { DEFAULT_RESOURCE_TYPE, ResourceType } from 'interfaces'; import { DEFAULT_RESOURCE_TYPE, ResourceType, SearchType } from 'interfaces';
import * as NavigationUtils from 'utils/navigationUtils'; import * as NavigationUtils from 'utils/navigationUtils';
import * as SearchUtils from 'ducks/search/utils'; import * as SearchUtils from 'ducks/search/utils';
...@@ -146,26 +146,30 @@ describe('search ducks', () => { ...@@ -146,26 +146,30 @@ describe('search ducks', () => {
const term = 'test'; const term = 'test';
const resource = ResourceType.table; const resource = ResourceType.table;
const pageIndex = 0; const pageIndex = 0;
const action = searchAll(term, resource, pageIndex); const searchType = SearchType.SUBMIT_TERM;
const action = searchAll(searchType, term, resource, pageIndex);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchAll.REQUEST); expect(action.type).toBe(SearchAll.REQUEST);
expect(payload.resource).toBe(resource); expect(payload.resource).toBe(resource);
expect(payload.term).toBe(term); expect(payload.term).toBe(term);
expect(payload.pageIndex).toBe(pageIndex); expect(payload.pageIndex).toBe(pageIndex);
expect(payload.useFilters).toBe(false); expect(payload.useFilters).toBe(false);
expect(payload.searchType).toBe(searchType);
}); });
it('searchAll - returns the action to search all resources with useFilters', () => { it('searchAll - returns the action to search all resources with useFilters', () => {
const term = 'test'; const term = 'test';
const resource = ResourceType.table; const resource = ResourceType.table;
const pageIndex = 0; const pageIndex = 0;
const action = searchAll(term, resource, pageIndex, true); const searchType = SearchType.SUBMIT_TERM;
const action = searchAll(searchType, term, resource, pageIndex, true);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchAll.REQUEST); expect(action.type).toBe(SearchAll.REQUEST);
expect(payload.resource).toBe(resource); expect(payload.resource).toBe(resource);
expect(payload.term).toBe(term); expect(payload.term).toBe(term);
expect(payload.pageIndex).toBe(pageIndex); expect(payload.pageIndex).toBe(pageIndex);
expect(payload.useFilters).toBe(true); expect(payload.useFilters).toBe(true);
expect(payload.searchType).toBe(searchType);
}); });
it('searchAllSuccess - returns the action to process the success', () => { it('searchAllSuccess - returns the action to process the success', () => {
...@@ -184,12 +188,14 @@ describe('search ducks', () => { ...@@ -184,12 +188,14 @@ describe('search ducks', () => {
const term = 'test'; const term = 'test';
const resource = ResourceType.table; const resource = ResourceType.table;
const pageIndex = 0; const pageIndex = 0;
const action = searchResource(term, resource, pageIndex); const searchType = SearchType.SUBMIT_TERM;
const action = searchResource(searchType, term, resource, pageIndex);
const { payload } = action; const { payload } = action;
expect(action.type).toBe(SearchResource.REQUEST); expect(action.type).toBe(SearchResource.REQUEST);
expect(payload.resource).toBe(resource); expect(payload.resource).toBe(resource);
expect(payload.term).toBe(term); expect(payload.term).toBe(term);
expect(payload.pageIndex).toBe(pageIndex); expect(payload.pageIndex).toBe(pageIndex);
expect(payload.searchType).toBe(searchType);
}); });
it('searchResourceSuccess - returns the action to process the success', () => { it('searchResourceSuccess - returns the action to process the success', () => {
...@@ -309,11 +315,9 @@ describe('search ducks', () => { ...@@ -309,11 +315,9 @@ describe('search ducks', () => {
expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState); expect(reducer(testState, { type: 'INVALID.ACTION' })).toEqual(testState);
}); });
it('should handle SearchAll.REQUEST', () => { it('should handle SearchAll.REQUEST', () => {
const term = 'testSearch'; const term = 'testSearch';
const resource = ResourceType.table; expect(reducer(testState, searchAll(SearchType.SUBMIT_TERM, term, ResourceType.table, 0))).toEqual({
const pageIndex = 0;
expect(reducer(testState, searchAll(term, resource, pageIndex))).toEqual({
...testState, ...testState,
inlineResults: initialInlineResultsState, inlineResults: initialInlineResultsState,
search_term: term, search_term: term,
...@@ -346,7 +350,7 @@ describe('search ducks', () => { ...@@ -346,7 +350,7 @@ describe('search ducks', () => {
}); });
it('should handle SearchResource.REQUEST', () => { it('should handle SearchResource.REQUEST', () => {
expect(reducer(testState, searchResource('test', ResourceType.table, 0))).toEqual({ expect(reducer(testState, searchResource(SearchType.SUBMIT_TERM, 'test', ResourceType.table, 0))).toEqual({
...initialState, ...initialState,
isLoading: true, isLoading: true,
}); });
...@@ -498,7 +502,7 @@ describe('search ducks', () => { ...@@ -498,7 +502,7 @@ describe('search ducks', () => {
updateSearchUrlSpy.mockClear(); updateSearchUrlSpy.mockClear();
saga = saga.next().select(SearchUtils.getSearchState).next(mockSearchState); saga = saga.next().select(SearchUtils.getSearchState).next(mockSearchState);
expect(getPageIndexSpy).toHaveBeenCalledWith(mockSearchState); expect(getPageIndexSpy).toHaveBeenCalledWith(mockSearchState);
saga = saga.put(searchResource(mockSearchState.search_term, mockSearchState.selectedTab, mockIndex)).next(); saga = saga.put(searchResource(SearchType.FILTER, mockSearchState.search_term, mockSearchState.selectedTab, mockIndex)).next();
expect(updateSearchUrlSpy).toHaveBeenCalledWith({ expect(updateSearchUrlSpy).toHaveBeenCalledWith({
filters: mockSearchState.filters, filters: mockSearchState.filters,
resource: mockSearchState.selectedTab, resource: mockSearchState.selectedTab,
...@@ -530,7 +534,7 @@ describe('search ducks', () => { ...@@ -530,7 +534,7 @@ describe('search ducks', () => {
*/ */
it('handles request error', () => { it('handles request error', () => {
testSaga(Sagas.searchAllWorker, searchAll('test', ResourceType.table, 0, true)) testSaga(Sagas.searchAllWorker, searchAll(SearchType.SUBMIT_TERM, 'test', ResourceType.table, 0, true))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(globalState.search).throw(new Error()).put(searchAllFailure()) .next(globalState.search).throw(new Error()).put(searchAllFailure())
.next().isDone(); .next().isDone();
...@@ -551,15 +555,16 @@ describe('search ducks', () => { ...@@ -551,15 +555,16 @@ describe('search ducks', () => {
const resource = ResourceType.table; const resource = ResourceType.table;
const term = 'test'; const term = 'test';
const mockSearchState = globalState.search; const mockSearchState = globalState.search;
testSaga(Sagas.searchResourceWorker, searchResource(term, resource, pageIndex)) const searchType = SearchType.PAGINATION;
testSaga(Sagas.searchResourceWorker, searchResource(searchType, term, resource, pageIndex))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(mockSearchState).call(API.searchResource, pageIndex, resource, term, mockSearchState.filters[resource]) .next(mockSearchState).call(API.searchResource, pageIndex, resource, term, mockSearchState.filters[resource], searchType)
.next(expectedSearchResults).put(searchResourceSuccess(expectedSearchResults)) .next(expectedSearchResults).put(searchResourceSuccess(expectedSearchResults))
.next().isDone(); .next().isDone();
}); });
it('handles request error', () => { it('handles request error', () => {
testSaga(Sagas.searchResourceWorker, searchResource('test', ResourceType.table, 0)) testSaga(Sagas.searchResourceWorker, searchResource(SearchType.PAGINATION, 'test', ResourceType.table, 0))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(globalState.search).throw(new Error()).put(searchResourceFailure()) .next(globalState.search).throw(new Error()).put(searchResourceFailure())
.next().isDone(); .next().isDone();
...@@ -573,7 +578,7 @@ describe('search ducks', () => { ...@@ -573,7 +578,7 @@ describe('search ducks', () => {
updateSearchUrlSpy.mockClear(); updateSearchUrlSpy.mockClear();
testSaga(Sagas.submitSearchWorker, submitSearch(term, true)) testSaga(Sagas.submitSearchWorker, submitSearch(term, true))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(mockSearchState).put(searchAll(term, undefined, undefined, true)) .next(mockSearchState).put(searchAll(SearchType.SUBMIT_TERM, term, undefined, undefined, true))
.next().isDone(); .next().isDone();
expect(updateSearchUrlSpy).toHaveBeenCalledWith({ term, filters: mockSearchState.filters }); expect(updateSearchUrlSpy).toHaveBeenCalledWith({ term, filters: mockSearchState.filters });
...@@ -632,7 +637,7 @@ describe('search ducks', () => { ...@@ -632,7 +637,7 @@ describe('search ducks', () => {
testSaga(Sagas.setPageIndexWorker, setPageIndex(index, updateUrl)) testSaga(Sagas.setPageIndexWorker, setPageIndex(index, updateUrl))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(searchState).put(searchResource(searchState.search_term, searchState.selectedTab, index)) .next(searchState).put(searchResource(SearchType.PAGINATION, searchState.search_term, searchState.selectedTab, index))
.next().isDone(); .next().isDone();
expect(updateSearchUrlSpy).toHaveBeenCalled(); expect(updateSearchUrlSpy).toHaveBeenCalled();
}); });
...@@ -644,7 +649,7 @@ describe('search ducks', () => { ...@@ -644,7 +649,7 @@ describe('search ducks', () => {
testSaga(Sagas.setPageIndexWorker, setPageIndex(index, updateUrl)) testSaga(Sagas.setPageIndexWorker, setPageIndex(index, updateUrl))
.next().select(SearchUtils.getSearchState) .next().select(SearchUtils.getSearchState)
.next(searchState).put(searchResource(searchState.search_term, searchState.selectedTab, index)) .next(searchState).put(searchResource(SearchType.PAGINATION, searchState.search_term, searchState.selectedTab, index))
.next().isDone(); .next().isDone();
expect(updateSearchUrlSpy).not.toHaveBeenCalled(); expect(updateSearchUrlSpy).not.toHaveBeenCalled();
}); });
...@@ -679,7 +684,7 @@ describe('search ducks', () => { ...@@ -679,7 +684,7 @@ describe('search ducks', () => {
it('Calls searchAll when search term changes', () => { it('Calls searchAll when search term changes', () => {
term = 'new search'; term = 'new search';
sagaTest(urlDidUpdate(`term=${term}&resource=${resource}&index=${index}`)) sagaTest(urlDidUpdate(`term=${term}&resource=${resource}&index=${index}`))
.put(searchAll(term, resource, index)) .put(searchAll(SearchType.LOAD_URL, term, resource, index))
.next().isDone(); .next().isDone();
}); });
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
DashboardResource, DashboardResource,
Resource, Resource,
ResourceType, ResourceType,
SearchType,
TableResource, TableResource,
UserResource, UserResource,
} from 'interfaces'; } from 'interfaces';
...@@ -53,6 +54,7 @@ export interface SearchAllRequest { ...@@ -53,6 +54,7 @@ export interface SearchAllRequest {
pageIndex: number; pageIndex: number;
term: string; term: string;
useFilters?: boolean; useFilters?: boolean;
searchType: SearchType;
}; };
type: SearchAll.REQUEST; type: SearchAll.REQUEST;
}; };
...@@ -75,6 +77,7 @@ export interface SearchResourceRequest { ...@@ -75,6 +77,7 @@ export interface SearchResourceRequest {
pageIndex: number; pageIndex: number;
resource: ResourceType; resource: ResourceType;
term: string; term: string;
searchType: SearchType;
}; };
type: SearchResource.REQUEST; type: SearchResource.REQUEST;
}; };
......
...@@ -7,3 +7,13 @@ export enum FilterType { ...@@ -7,3 +7,13 @@ export enum FilterType {
CHECKBOX_SELECT = 'checkboxFilter', CHECKBOX_SELECT = 'checkboxFilter',
INPUT_SELECT = 'inputFilter' INPUT_SELECT = 'inputFilter'
} }
export enum SearchType {
CLEAR_TERM = 'clear_search_term',
FILTER = 'update_filter',
INLINE_SEARCH = 'inline_search',
INLINE_SELECT = 'inline_select',
LOAD_URL = 'load_url',
PAGINATION = 'update_page',
SUBMIT_TERM = 'submit_search_term',
}
...@@ -6,7 +6,7 @@ from http import HTTPStatus ...@@ -6,7 +6,7 @@ from http import HTTPStatus
from unittest.mock import patch from unittest.mock import patch
from amundsen_application import create_app from amundsen_application import create_app
from amundsen_application.api.search.v0 import _create_url_with_field, SEARCH_ENDPOINT, SEARCH_USER_ENDPOINT from amundsen_application.api.search.v0 import SEARCH_TABLE_ENDPOINT, SEARCH_USER_ENDPOINT
local_app = create_app('amundsen_application.config.TestConfig', 'tests/templates') local_app = create_app('amundsen_application.config.TestConfig', 'tests/templates')
...@@ -49,7 +49,8 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -49,7 +49,8 @@ class SearchTableQueryString(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.mock_table_results = MOCK_TABLE_RESULTS self.mock_table_results = MOCK_TABLE_RESULTS
self.expected_parsed_table_results = MOCK_PARSED_TABLE_RESULTS self.expected_parsed_table_results = MOCK_PARSED_TABLE_RESULTS
self.search_url = local_app.config['SEARCHSERVICE_BASE'] + '/search_table' self.search_service_url = local_app.config['SEARCHSERVICE_BASE'] + SEARCH_TABLE_ENDPOINT
self.fe_flask_endpoint = '/api/search/v0/table'
def test_fail_if_term_is_none(self) -> None: def test_fail_if_term_is_none(self) -> None:
""" """
...@@ -57,7 +58,7 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -57,7 +58,7 @@ class SearchTableQueryString(unittest.TestCase):
:return: :return:
""" """
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.post('/api/search/v0/table_qs', json={'pageIndex': 0}) response = test.post(self.fe_flask_endpoint, json={'pageIndex': 0})
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
def test_fail_if_page_index_is_none(self) -> None: def test_fail_if_page_index_is_none(self) -> None:
...@@ -66,9 +67,35 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -66,9 +67,35 @@ class SearchTableQueryString(unittest.TestCase):
:return: :return:
""" """
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.post('/api/search/v0/table_qs', json={'term': ''}) response = test.post(self.fe_flask_endpoint, json={'term': ''})
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate
@patch('amundsen_application.api.search.v0._search_table')
def test_calls_search_table_log_helper(self, search_table_mock) -> None:
"""
Test _search_table helper method is called with correct arguments for logging
from the request_json
:return:
"""
test_filters = {'schema': 'test_schema'}
test_term = 'hello'
test_index = 1
test_search_type = 'test'
responses.add(responses.POST, self.search_service_url, json=self.mock_table_results, status=HTTPStatus.OK)
with local_app.test_client() as test:
test.post(self.fe_flask_endpoint,
json={
'term': test_term,
'pageIndex': test_index,
'filters': test_filters,
'searchType': test_search_type})
search_table_mock.assert_called_with(filters=test_filters,
page_index=test_index,
search_term=test_term,
search_type=test_search_type)
@responses.activate @responses.activate
@patch('amundsen_application.api.search.v0.generate_query_json') @patch('amundsen_application.api.search.v0.generate_query_json')
def test_calls_generate_query_json(self, mock_generate_query_json) -> None: def test_calls_generate_query_json(self, mock_generate_query_json) -> None:
...@@ -80,10 +107,10 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -80,10 +107,10 @@ class SearchTableQueryString(unittest.TestCase):
test_filters = {'schema': 'test_schema'} test_filters = {'schema': 'test_schema'}
test_term = 'hello' test_term = 'hello'
test_index = 1 test_index = 1
responses.add(responses.POST, self.search_url, json=self.mock_table_results, status=HTTPStatus.OK) responses.add(responses.POST, self.search_service_url, json=self.mock_table_results, status=HTTPStatus.OK)
with local_app.test_client() as test: with local_app.test_client() as test:
test.post('/api/search/v0/table_qs', test.post(self.fe_flask_endpoint,
json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters}) json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters})
mock_generate_query_json.assert_called_with(filters=test_filters, mock_generate_query_json.assert_called_with(filters=test_filters,
page_index=test_index, page_index=test_index,
...@@ -102,7 +129,7 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -102,7 +129,7 @@ class SearchTableQueryString(unittest.TestCase):
mock_generate_query_json.side_effect = Exception('Test exception') mock_generate_query_json.side_effect = Exception('Test exception')
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.post('/api/search/v0/table_qs', response = test.post(self.fe_flask_endpoint,
json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters}) json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters})
data = json.loads(response.data) data = json.loads(response.data)
self.assertEqual(data.get('msg'), 'Encountered exception generating query json: Test exception') self.assertEqual(data.get('msg'), 'Encountered exception generating query json: Test exception')
...@@ -117,10 +144,10 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -117,10 +144,10 @@ class SearchTableQueryString(unittest.TestCase):
test_filters = {'schema': 'test_schema'} test_filters = {'schema': 'test_schema'}
test_term = 'hello' test_term = 'hello'
test_index = 1 test_index = 1
responses.add(responses.POST, self.search_url, json=self.mock_table_results, status=HTTPStatus.OK) responses.add(responses.POST, self.search_service_url, json=self.mock_table_results, status=HTTPStatus.OK)
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.post('/api/search/v0/table_qs', response = test.post(self.fe_flask_endpoint,
json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters}) json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters})
data = json.loads(response.data) data = json.loads(response.data)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
...@@ -138,20 +165,18 @@ class SearchTableQueryString(unittest.TestCase): ...@@ -138,20 +165,18 @@ class SearchTableQueryString(unittest.TestCase):
test_filters = {'schema': 'test_schema'} test_filters = {'schema': 'test_schema'}
test_term = 'hello' test_term = 'hello'
test_index = 1 test_index = 1
responses.add(responses.POST, self.search_url, json={}, status=HTTPStatus.BAD_REQUEST) responses.add(responses.POST, self.search_service_url, json={}, status=HTTPStatus.BAD_REQUEST)
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.post('/api/search/v0/table_qs', response = test.post(self.fe_flask_endpoint,
json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters}) json={'term': test_term, 'pageIndex': test_index, 'filters': test_filters})
data = json.loads(response.data) data = json.loads(response.data)
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
self.assertEqual(data.get('msg'), 'Encountered error: Search request failed') self.assertEqual(data.get('msg'), 'Encountered error: Search request failed')
class SearchTest(unittest.TestCase): class SearchUserTest(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.mock_search_table_results = MOCK_TABLE_RESULTS
self.expected_parsed_search_table_results = MOCK_PARSED_TABLE_RESULTS
self.mock_search_user_results = { self.mock_search_user_results = {
'total_results': 1, 'total_results': 1,
'results': [ 'results': [
...@@ -194,114 +219,7 @@ class SearchTest(unittest.TestCase): ...@@ -194,114 +219,7 @@ class SearchTest(unittest.TestCase):
'total_results': 1, 'total_results': 1,
'results': 'Bad results to trigger exception' 'results': 'Bad results to trigger exception'
} }
self.fe_flask_endpoint = '/api/search/v0/user'
# ----- Table Search Tests ---- #
def test_search_table_fail_if_no_query(self) -> None:
"""
Test request failure if 'query' is not provided in the query string
to the search endpoint
:return:
"""
with local_app.test_client() as test:
response = test.get('/api/search/v0/table', query_string=dict(page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
def test_search_table_fail_if_no_page_index(self) -> None:
"""
Test request failure if 'page_index' is not provided in the query string
to the search endpoint
:return:
"""
with local_app.test_client() as test:
response = test.get('/api/search/v0/table', query_string=dict(query='test'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate
def test_search_table_success(self) -> None:
"""
Test request success
:return:
"""
responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
json=self.mock_search_table_results, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0'))
data = json.loads(response.data)
self.assertEqual(response.status_code, HTTPStatus.OK)
tables = data.get('tables')
self.assertEqual(tables.get('total_results'), self.mock_search_table_results.get('total_results'))
self.assertCountEqual(tables.get('results'), self.expected_parsed_search_table_results)
@responses.activate
def test_search_table_fail_on_non_200_response(self) -> None:
"""
Test request failure if search endpoint returns non-200 http code
:return:
"""
responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
json=self.mock_search_table_results, status=HTTPStatus.INTERNAL_SERVER_ERROR)
with local_app.test_client() as test:
response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate
def test_search_table_fail_on_proccessing_bad_response(self) -> None:
"""
Test catching exception if there is an error processing the results
from the search endpoint
:return:
"""
responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
json=self.bad_search_results, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/search/v0/table', query_string=dict(query='test', page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate
def test_search_table_with_field(self) -> None:
"""
Test search request if user search with colon
:return:
"""
responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_ENDPOINT,
json={}, status=HTTPStatus.OK)
with local_app.test_client() as test:
response = test.get('/api/search/field/'
'tag_names/field_val/test', query_string=dict(query_term='test',
page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.OK)
def test_create_url_with_field(self) -> None:
# test with invalid search term
with self.assertRaises(Exception):
invalid_search_term1 = 'tag:hive & schema:default test'
_create_url_with_field(search_term=invalid_search_term1,
page_index=1)
invalid_search_term2 = 'tag1:hive tag'
_create_url_with_field(search_term=invalid_search_term2,
page_index=1)
with local_app.app_context():
# test single tag with query term
search_term = 'tag:hive test_table'
expected = local_app.config['SEARCHSERVICE_BASE'] + \
'/search/field/tag/field_val/hive?page_index=1&query_term=test_table'
self.assertEqual(_create_url_with_field(search_term=search_term,
page_index=1), expected)
# test single tag without query term
search_term = 'tag:hive'
expected = local_app.config['SEARCHSERVICE_BASE'] + \
'/search/field/tag/field_val/hive?page_index=1'
self.assertEqual(_create_url_with_field(search_term=search_term,
page_index=1), expected)
def test_search_user_fail_if_no_query(self) -> None: def test_search_user_fail_if_no_query(self) -> None:
""" """
...@@ -310,7 +228,7 @@ class SearchTest(unittest.TestCase): ...@@ -310,7 +228,7 @@ class SearchTest(unittest.TestCase):
:return: :return:
""" """
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.get('/api/search/v0/user', query_string=dict(page_index='0')) response = test.get(self.fe_flask_endpoint, query_string=dict(page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
def test_search_user_fail_if_no_page_index(self) -> None: def test_search_user_fail_if_no_page_index(self) -> None:
...@@ -320,7 +238,7 @@ class SearchTest(unittest.TestCase): ...@@ -320,7 +238,7 @@ class SearchTest(unittest.TestCase):
:return: :return:
""" """
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.get('/api/search/v0/user', query_string=dict(query='test')) response = test.get(self.fe_flask_endpoint, query_string=dict(query='test'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
@responses.activate @responses.activate
...@@ -333,12 +251,12 @@ class SearchTest(unittest.TestCase): ...@@ -333,12 +251,12 @@ class SearchTest(unittest.TestCase):
json=self.mock_search_user_results, status=HTTPStatus.OK) json=self.mock_search_user_results, status=HTTPStatus.OK)
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.get('/api/search/v0/user', query_string=dict(query='test', page_index='0')) response = test.get(self.fe_flask_endpoint, query_string=dict(query='test', page_index='0'))
data = json.loads(response.data) data = json.loads(response.data)
self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.status_code, HTTPStatus.OK)
users = data.get('users') users = data.get('users')
self.assertEqual(users.get('total_results'), self.mock_search_table_results.get('total_results')) self.assertEqual(users.get('total_results'), self.mock_search_user_results.get('total_results'))
self.assertCountEqual(users.get('results'), self.expected_parsed_search_user_results) self.assertCountEqual(users.get('results'), self.expected_parsed_search_user_results)
@responses.activate @responses.activate
...@@ -348,8 +266,8 @@ class SearchTest(unittest.TestCase): ...@@ -348,8 +266,8 @@ class SearchTest(unittest.TestCase):
:return: :return:
""" """
responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_USER_ENDPOINT, responses.add(responses.GET, local_app.config['SEARCHSERVICE_BASE'] + SEARCH_USER_ENDPOINT,
json=self.mock_search_table_results, status=HTTPStatus.INTERNAL_SERVER_ERROR) json=self.mock_search_user_results, status=HTTPStatus.INTERNAL_SERVER_ERROR)
with local_app.test_client() as test: with local_app.test_client() as test:
response = test.get('/api/search/v0/user', query_string=dict(query='test', page_index='0')) response = test.get(self.fe_flask_endpoint, query_string=dict(query='test', page_index='0'))
self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR) self.assertEqual(response.status_code, HTTPStatus.INTERNAL_SERVER_ERROR)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment