Reworked the URL handler to accept a hashed ID instead of a raw ID.

Added docs, tests and code to enable promoted GUIDs to be passed in through the TAAR API
This commit is contained in:
Victor Ng 2018-11-27 12:37:41 -05:00
Родитель bab992b682
Коммит 3183eab590
3 изменённых файлов: 243 добавлений и 3 удалений

94
API.md Normal file
Просмотреть файл

@ -0,0 +1,94 @@
API documentation
# Get addon recommendations
Allow the Authenticated User to update their details.
**URL** : `/v1/api/recommendations/<hashed_id>/`
**Method** : `POST`
**Auth required** : NO
**Permissions required** : None
**Data constraints**
```json
{
"options": {"promoted": [
["[1 to 30 chars]", Some Number],
["[1 to 30 chars]", Some Number],
]
}
}
```
Note that the only valid key for the top level JSON is `options`.
`options` is always a dictionary of optional values.
To denote no optional data - it is perfectly valid for the JSON data
to have no `options` key, or even simpler - not have POST data at all.
Each item in the promoted addon GUID list is accompanied by an
integer weight. Any weight is greater than a TAAR recommended addon
GUID.
**Data examples**
Partial data is allowed.
```json
{
"options": {"promoted": [
["guid1", 10],
["guid2", 5],
]
}
}
```
## Success Responses
**Condition** : Data provided is valid
**Code** : `200 OK`
**Content example** : Response will reflect a list of addon GUID suggestions.
```json
{
"results": ["taar-guid1", "taar-guid2", "taar-guid3"],
"result_info": [],
}
```
## Error Response
**Condition** : If provided data is invalid, e.g. options object is not a dictionary.
**Code** : `400 BAD REQUEST`
**Content example** :
```json
{
"invalid_option": [
"Please provide a dictionary with a `promoted` key mapped to a list of promoted addon GUIDs",
]
}
```
## Notes
* Endpoint will ignore irrelevant and read-only data such as parameters that
don't exist, or fields.
* Endpoint will try to fail gracefully and return an empty list in the
results key if no suggestions can be made.
* The only condition when the endpoint should return an error code if
the options data is malformed.

Просмотреть файл

@ -34,14 +34,37 @@ def configure_plugin(app): # noqa: C901
This is a factory function that configures all the routes for
flask given a particular library.
"""
@app.route('/api/recommendations/<uuid:uuid_client_id>/')
def recommendations(uuid_client_id):
@app.route("/v1/api/recommendations/<hashed_client_id>/", methods=["GET", "POST"])
def recommendations(hashed_client_id):
"""Return a list of recommendations provided a telemetry client_id."""
# Use the module global PROXY_MANAGER
global PROXY_MANAGER
try:
promoted_guids = []
if request.method == "POST":
json_data = request.data
# At least Python3.5 returns request.data as bytes
# type instead of a string type.
# Both Python2.7 and Python3.7 return a string type
if type(json_data) == bytes:
json_data = json_data.decode("utf8")
post_data = json.loads(json_data)
promoted_guids = post_data.get("options", {}).get("promoted", [])
if promoted_guids:
promoted_guids.sort(key=lambda x: x[1], reverse=True)
promoted_guids = [x[0] for x in promoted_guids]
except Exception as e:
return app.response_class(
response=json.dumps({"error": "Invalid JSON in POST: {}".format(e)}),
status=400,
mimetype="application/json",
)
# Coerce the uuid.UUID type into a string
client_id = str(uuid_client_id)
client_id = str(hashed_client_id)
branch = request.args.get("branch", "")
@ -76,6 +99,7 @@ def configure_plugin(app): # noqa: C901
# Strip out weights from TAAR results to maintain compatibility
# with TAAR 1.0
jdata = {"results": [x[0] for x in recommendations]}
jdata["results"] = (promoted_guids + jdata["results"])[:TAAR_MAX_RESULTS]
response = app.response_class(
response=json.dumps(jdata), status=200, mimetype="application/json"

Просмотреть файл

@ -3,7 +3,15 @@ from taar.context import default_context
from taar import ProfileFetcher
from taar.profile_fetcher import ProfileController
from taar import recommenders
from flask import url_for
import time
from flask import Flask
import uuid
try:
from unittest.mock import MagicMock
except Exception:
from mock import MagicMock
def create_recommendation_manager():
@ -17,6 +25,44 @@ def create_recommendation_manager():
return rm
@pytest.fixture
def app():
from taar.plugin import configure_plugin
from taar.plugin import PROXY_MANAGER
flask_app = Flask("test")
# Clobber the default recommendation manager with a MagicMock
mock_recommender = MagicMock()
PROXY_MANAGER.setResource(mock_recommender)
configure_plugin(flask_app)
return flask_app
def test_empty_results_by_default(client, app):
# The default behaviour under test should be that the
# RecommendationManager simply no-ops everything so we get back an
# empty result list.
res = client.get("/v1/api/recommendations/not_a_real_hash/")
assert res.json == {"results": []}
def test_only_promoted_addons(client, app):
# POSTing a JSON blob allows us to specify promoted addons to the
# TAAR service.
res = client.post(
"/v1/api/recommendations/not_a_real_hash/",
json=dict(
{"options": {"promoted": [["guid1", 10], ["guid2", 5], ["guid55", 8]]}}
),
follow_redirects=True,
)
# The result should order the GUIDs in descending order of weight
assert res.json == {"results": ["guid1", "guid55", "guid2"]}
@pytest.mark.skip("This is an integration test")
def test_recommenders(client_id="some_dev_client_id", branch="linear"):
"""
@ -76,3 +122,79 @@ def micro_bench(x, client_id, branch_label):
end = time.time()
print(("%0.5f seconds per request" % ((end - start) / x)))
@pytest.fixture
def empty_recommendation_manager(monkeypatch):
# TODO: Clobbering the recommendationmanager really needs to be
# simplified
return None
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_empty_recommendation(client, empty_recommendation_manager):
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
assert response.data == b'{"results": []}'
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_locale_recommendation(client, locale_recommendation_manager):
response = client.get(
url_for("recommendations", uuid_client_id=uuid.uuid4()) + "?locale=en-US"
)
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
assert response.data == b'{"results": ["addon-Locale"]}'
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
assert response.data == b'{"results": []}'
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_platform_recommendation(client, platform_recommendation_manager):
uri = (
url_for("recommendations", uuid_client_id=str(uuid.uuid4())) + "?platform=WOW64"
)
response = client.get(uri)
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
assert response.data == b'{"results": ["addon-WOW64"]}'
response = client.get(url_for("recommendations", uuid_client_id=uuid.uuid4()))
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
assert response.data == b'{"results": []}'
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_intervention_a(client, static_recommendation_manager):
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
response = client.get(url + "?branch=intervention-a")
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
expected = b'{"results": ["intervention-a-addon-1", "intervention-a-addon-2", "intervention-a-addon-N"]}'
assert response.data == expected
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_intervention_b(client, static_recommendation_manager):
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
response = client.get(url + "?branch=intervention_b")
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
expected = b'{"results": ["intervention_b-addon-1", "intervention_b-addon-2", "intervention_b-addon-N"]}'
assert response.data == expected
@pytest.mark.skip("disabled until plugin system for taar-api is cleaned up")
def test_control_branch(client, static_recommendation_manager):
url = url_for("recommendations", uuid_client_id=uuid.uuid4())
response = client.get(url + "?branch=control")
assert response.status_code == 200
assert response.headers["Content-Type"] == "application/json"
expected = b'{"results": ["control-addon-1", "control-addon-2", "control-addon-N"]}'
assert response.data == expected