Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add labels collection and endpoints #1892

Merged
merged 10 commits into from Sep 25, 2020
Empty file added tests/labels/__init__.py
Empty file.
196 changes: 196 additions & 0 deletions tests/labels/test_api.py
@@ -0,0 +1,196 @@
import pytest


async def test_find(spawn_client):
"""
Test that a ``GET /api/labels`` return a complete list of labels.

"""
client = await spawn_client(authorize=True, administrator=True)
await client.db.labels.insert_many([
{
"_id": "test_1",
"name": "Bug",
"color": "#a83432"
},
{
"_id": "test_2",
"name": "Question",
"color": "#03fc20"
}
])

resp = await client.get("/api/labels")
assert resp.status == 200

assert await resp.json() == [
{
"id": "test_1",
"name": "Bug",
"color": "#a83432"
},
{
"id": "test_2",
"name": "Question",
"color": "#03fc20"
}
]


@pytest.mark.parametrize("error", [None, "404"])
async def test_get(error, spawn_client, all_permissions, resp_is):
"""
Test that a ``GET /api/labels/:label_id`` return the correct label document.

"""
client = await spawn_client(authorize=True, administrator=True)

if not error:
await client.db.labels.insert_one({
"_id": "test",
"name": "Bug",
"color": "#a83432"
})

resp = await client.get("/api/labels/test")

if error:
assert await resp_is.not_found(resp)
return

assert resp.status == 200

assert await resp.json() == {
"id": "test",
"name": "Bug",
"color": "#a83432"
}


@pytest.mark.parametrize("error", [None, "400_exists", "400_color"])
async def test_create(error, spawn_client, test_random_alphanumeric, resp_is):
"""
Test that a label can be added to the database at ``POST /api/labels``.

"""
client = await spawn_client(authorize=True, administrator=True)

if error == "400_exists":
await client.db.labels.insert_one({
"name": "Bug"
})

data = {
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
}

if error == "400_color":
data["color"] = "#1234567"

resp = await client.post("/api/labels", data)

if error == "400_exists":
assert await resp_is.bad_request(resp, "Label name already exists")
return

if error == "400_color":
assert await resp_is.bad_request(resp, "This is not a valid Hexadecimal color")
return

assert resp.status == 201

expected_id = test_random_alphanumeric.history[0]
assert resp.headers["Location"] == "/api/labels/" + expected_id

assert await resp.json() == {
"id": expected_id,
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
}


@pytest.mark.parametrize("error", [None, "404", "400_exists", "400_color"])
async def test_edit(error, spawn_client, resp_is):
"""
Test that a label can be edited to the database at ``PATCH /api/labels/:label_id``.

"""
client = await spawn_client(authorize=True, administrator=True)

if error != "404":
await client.db.labels.insert_many([
{
"_id": "test_1",
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
},
{
"_id": "test_2",
"name": "Question",
"color": "#32a85f",
"description": "Question from a user"
}
])

data = {
"name": "Bug",
"color": "#fc5203",
"description": "Need to be fixed"
}

if error == "400_exists":
data["name"] = "Question"

if error == "400_color":
data["color"] = "#123bzp"

resp = await client.patch("/api/labels/test_1", data=data)

if error == "404":
assert await resp_is.not_found(resp)
return

if error == "400_exists":
assert await resp_is.bad_request(resp, "Label name already exists")
return

if error == "400_color":
assert await resp_is.bad_request(resp, "This is not a valid Hexadecimal color")
return

assert resp.status == 200
assert await resp.json() == {
"id": "test_1",
"name": "Bug",
"color": "#fc5203",
"description": "Need to be fixed"
}


@pytest.mark.parametrize("error", [None, "400"])
async def test_remove(error, spawn_client, resp_is):
"""
Test that a label can be deleted to the database at ``DELETE /api/labels/:label_id``.

"""
client = await spawn_client(authorize=True, administrator=True)

if not error:
await client.db.labels.insert_one({
"_id": "test",
"name": "Bug",
"color": "#a83432",
"description": "This is a bug"
})

resp = await client.delete("/api/labels/test")

if error:
assert await resp_is.not_found(resp)
return

assert await resp_is.no_content(resp)
2 changes: 2 additions & 0 deletions virtool/db/core.py
Expand Up @@ -268,6 +268,8 @@ def __init__(self, client, enqueue_change):

self.groups = self.bind_collection("groups")

self.labels = self.bind_collection("labels")

self.history = self.bind_collection(
"history",
projection=virtool.history.db.PROJECTION
Expand Down
3 changes: 2 additions & 1 deletion virtool/dispatcher.py
Expand Up @@ -40,7 +40,8 @@
"software",
"status",
"subtraction",
"users"
"users",
"labels"
igboyes marked this conversation as resolved.
Show resolved Hide resolved
)

#: Allowed operations. Calls to :meth:`.Dispatcher.dispatch` will be validated against these operations.
Expand Down
Empty file added virtool/labels/__init__.py
Empty file.
153 changes: 153 additions & 0 deletions virtool/labels/api.py
@@ -0,0 +1,153 @@
import virtool.http.routes
import virtool.utils
import virtool.validators
import virtool.labels.checks
import virtool.db.utils
from virtool.api.response import bad_request, json_response, no_content, not_found

routes = virtool.http.routes.Routes()


@routes.get("/api/labels")
async def find(req):
"""
Get a list of all label documents in the database.

"""
db = req.app["db"]

document = db.labels.find()
igboyes marked this conversation as resolved.
Show resolved Hide resolved

return json_response([virtool.utils.base_processor(d) async for d in document])


@routes.get("/api/labels/{label_id}")
async def get(req):
"""
Get a complete label document.

"""
document = await req.app["db"].labels.find_one(req.match_info["label_id"])

if not document:
igboyes marked this conversation as resolved.
Show resolved Hide resolved
return not_found()

return json_response(virtool.utils.base_processor(document))


@routes.post("/api/labels", schema={
"name": {
"type": "string",
"coerce": virtool.validators.strip,
"required": True,
"empty": False
},
"color": {
"type": "string",
"coerce": virtool.validators.strip,
},
"description": {
"type": "string",
"coerce": virtool.validators.strip,
"default": ""
}
})
async def create(req):
"""
Add a new label to the labels database.

"""
db = req.app["db"]
data = req["data"]

valid_color = await virtool.labels.checks.check_hex_color(req)

if not valid_color:
igboyes marked this conversation as resolved.
Show resolved Hide resolved
return bad_request("This is not a valid Hexadecimal color")

name_exist = await db.labels.count_documents({'name': data['name']})
igboyes marked this conversation as resolved.
Show resolved Hide resolved

if name_exist:
return bad_request("Label name already exists")

label_id = await virtool.db.utils.get_new_id(db.labels)

document = {
"_id": label_id,
"name": data["name"],
"color": data["color"],
"description": data["description"]
}

await db.labels.insert_one(document)

headers = {
"Location": "/api/labels/" + label_id
igboyes marked this conversation as resolved.
Show resolved Hide resolved
}

return json_response(virtool.utils.base_processor(document), status=201, headers=headers)


@routes.patch("/api/labels/{label_id}", schema={
"name": {
"type": "string",
"coerce": virtool.validators.strip,
},
"color": {
"type": "string",
"coerce": virtool.validators.strip,
},
"description": {
"type": "string",
"coerce": virtool.validators.strip,
}
})
async def edit(req):
"""
Edit an existing label.

"""
db = req.app["db"]
data = req["data"]

label_id = req.match_info["label_id"]

if data["name"]:
igboyes marked this conversation as resolved.
Show resolved Hide resolved
name_exist = await db.labels.count_documents({"_id": {"$ne": label_id}, "name": data["name"]})

if name_exist:
return bad_request("Label name already exists")

if data["color"]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will throw KeyError if color not in request JSON. Watch out for this in general.

valid_color = await virtool.labels.checks.check_hex_color(req)
igboyes marked this conversation as resolved.
Show resolved Hide resolved

if not valid_color:
return bad_request("This is not a valid Hexadecimal color")

document = await db.labels.find_one_and_update({"_id": label_id}, {
"$set": data
})

if document is None:
return not_found()

return json_response(virtool.utils.base_processor(document))


@routes.delete("/api/labels/{label_id}")
async def remove(req):
"""
Remove a label.

"""
db = req.app["db"]

label_id = req.match_info["label_id"]

delete_result = await db.labels.delete_one({"_id": label_id})

if delete_result.deleted_count == 0:
return not_found()

return no_content()

14 changes: 14 additions & 0 deletions virtool/labels/checks.py
@@ -0,0 +1,14 @@
import aiohttp.web
import re


async def check_hex_color(req: aiohttp.web.Request):
"""
Check if the Hex color in the request is valid.

"""
regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"

match = re.match(regex, req["data"]["color"])
igboyes marked this conversation as resolved.
Show resolved Hide resolved

return match