🐛(api) enable search on identites instead of users
A previous PR enabled user search using the email. After discussion models, we chose to enable research on identities, while still returning users.
This commit is contained in:
committed by
Marie
parent
5b0b2933a2
commit
3aba9a4419
@@ -180,8 +180,8 @@ class UserViewSet(
|
||||
|
||||
GET /api/users/&q=query
|
||||
Return a list of users whose email matches the query. Similarity is
|
||||
calculated using trigram similarity, allowing for partial, case
|
||||
insensitive matches and accentuated queries.
|
||||
calculated using trigram similarity, allowing for partial,
|
||||
case-insensitive matches and accented queries.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.IsSelf]
|
||||
@@ -203,7 +203,7 @@ class UserViewSet(
|
||||
# Search by case-insensitive and accent-insensitive trigram similarity
|
||||
if query := self.request.GET.get("q", ""):
|
||||
similarity = TrigramSimilarity(
|
||||
Func("email", function="unaccent"),
|
||||
Func("identities__email", function="unaccent"),
|
||||
Func(Value(query), function="unaccent"),
|
||||
)
|
||||
queryset = (
|
||||
|
||||
@@ -4,7 +4,12 @@ Test users API endpoints in the People core app.
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
HTTP_403_FORBIDDEN,
|
||||
HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories, models
|
||||
@@ -21,7 +26,7 @@ def test_api_users_list_anonymous():
|
||||
factories.UserFactory()
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/users/")
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication credentials were not provided." in response.content.decode(
|
||||
"utf-8"
|
||||
)
|
||||
@@ -38,7 +43,7 @@ def test_api_users_list_authenticated():
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert len(response.json()["results"]) == 3
|
||||
|
||||
|
||||
@@ -51,10 +56,10 @@ def test_api_users_authenticated_list_by_email():
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.UserFactory(email="david.bowman@work.com")
|
||||
nicole = factories.UserFactory(email="nicole_foole@work.com")
|
||||
frank = factories.UserFactory(email="frank_poole@work.com")
|
||||
factories.UserFactory(email="heywood_floyd@work.com")
|
||||
dave = factories.IdentityFactory(email="david.bowman@work.com")
|
||||
nicole = factories.IdentityFactory(email="nicole_foole@work.com")
|
||||
frank = factories.IdentityFactory(email="frank_poole@work.com")
|
||||
factories.IdentityFactory(email="heywood_floyd@work.com")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
@@ -64,35 +69,61 @@ def test_api_users_authenticated_list_by_email():
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids[0] == str(dave.id)
|
||||
assert user_ids[0] == str(dave.user.id)
|
||||
|
||||
# Partial query should work
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=fran", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids[0] == str(frank.id)
|
||||
assert user_ids[0] == str(frank.user.id)
|
||||
|
||||
# Result that matches a trigram twice ranks better than result that matches once
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=ole", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
# "Nicole Foole" matches twice on "ole"
|
||||
assert user_ids == [str(nicole.id), str(frank.id)]
|
||||
assert user_ids == [str(nicole.user.id), str(frank.user.id)]
|
||||
|
||||
# Even with a low similarity threshold, query should match expected emails
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=ool", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(nicole.id), str(frank.id)]
|
||||
assert user_ids == [str(nicole.user.id), str(frank.user.id)]
|
||||
|
||||
|
||||
def test_api_users_authenticated_list_multiplie_identities_user():
|
||||
"""
|
||||
Authenticated users should be able to search users with a case-insensitive and
|
||||
partial query on the email.
|
||||
"""
|
||||
user = factories.UserFactory(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.UserFactory()
|
||||
factories.IdentityFactory(user=dave, email="david.bowman@work.com")
|
||||
factories.IdentityFactory(user=dave, email="david.bowman@fun.fr")
|
||||
|
||||
# Full query should work
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=david.bowman@work.com",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_200_OK
|
||||
# A single user is returned, despite similarity matching both emails
|
||||
assert response.json()["count"] == 1
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids[0] == str(dave.id)
|
||||
|
||||
|
||||
def test_api_users_authenticated_list_uppercase_content():
|
||||
@@ -101,7 +132,7 @@ def test_api_users_authenticated_list_uppercase_content():
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.UserFactory(email="DAVID.BOWMAN@INTENSEWORK.COM")
|
||||
dave = factories.IdentityFactory(email="DAVID.BOWMAN@INTENSEWORK.COM")
|
||||
|
||||
# Unaccented full address
|
||||
response = APIClient().get(
|
||||
@@ -109,18 +140,18 @@ def test_api_users_authenticated_list_uppercase_content():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
# Partial query
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=david", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
|
||||
def test_api_users_list_authenticated_capital_query():
|
||||
@@ -129,7 +160,7 @@ def test_api_users_list_authenticated_capital_query():
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
dave = factories.UserFactory(email="david.bowman@work.com")
|
||||
dave = factories.IdentityFactory(email="david.bowman@work.com")
|
||||
|
||||
# Full uppercase query
|
||||
response = APIClient().get(
|
||||
@@ -137,18 +168,18 @@ def test_api_users_list_authenticated_capital_query():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
# Partial uppercase email
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=DAVID", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(dave.id)]
|
||||
assert user_ids == [str(dave.user.id)]
|
||||
|
||||
|
||||
def test_api_contacts_list_authenticated_accented_query():
|
||||
@@ -157,7 +188,7 @@ def test_api_contacts_list_authenticated_accented_query():
|
||||
factories.IdentityFactory(user=user, email=user.email)
|
||||
jwt_token = OIDCToken.for_user(user)
|
||||
|
||||
helene = factories.UserFactory(email="helene.bowman@work.com")
|
||||
helene = factories.IdentityFactory(email="helene.bowman@work.com")
|
||||
|
||||
# Accented full query
|
||||
response = APIClient().get(
|
||||
@@ -165,18 +196,18 @@ def test_api_contacts_list_authenticated_accented_query():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(helene.id)]
|
||||
assert user_ids == [str(helene.user.id)]
|
||||
|
||||
# Unaccented partial email
|
||||
response = APIClient().get(
|
||||
"/api/v1.0/users/?q=hélène", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user_ids = [user["id"] for user in response.json()["results"]]
|
||||
assert user_ids == [str(helene.id)]
|
||||
assert user_ids == [str(helene.user.id)]
|
||||
|
||||
|
||||
@mock.patch.object(Pagination, "get_page_size", return_value=3)
|
||||
@@ -223,7 +254,7 @@ def test_api_users_retrieve_me_anonymous():
|
||||
factories.UserFactory.create_batch(2)
|
||||
client = APIClient()
|
||||
response = client.get("/api/v1.0/users/me/")
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
@@ -245,7 +276,7 @@ def test_api_users_retrieve_me_authenticated():
|
||||
"/api/v1.0/users/me/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response.json() == {
|
||||
"id": str(user.id),
|
||||
"email": str(user.email),
|
||||
@@ -263,7 +294,7 @@ def test_api_users_retrieve_anonymous():
|
||||
user = factories.UserFactory()
|
||||
response = client.get(f"/api/v1.0/users/{user.id!s}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
@@ -281,7 +312,7 @@ def test_api_users_retrieve_authenticated_self():
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/users/{user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
|
||||
|
||||
@@ -298,7 +329,7 @@ def test_api_users_retrieve_authenticated_other():
|
||||
response = APIClient().get(
|
||||
f"/api/v1.0/users/{other_user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "GET" not allowed.'}
|
||||
|
||||
|
||||
@@ -311,7 +342,7 @@ def test_api_users_create_anonymous():
|
||||
"password": "mypassword",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert "Authentication credentials were not provided." in response.content.decode(
|
||||
"utf-8"
|
||||
)
|
||||
@@ -333,7 +364,7 @@ def test_api_users_create_authenticated():
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert response.json() == {"detail": 'Method "POST" not allowed.'}
|
||||
assert models.User.objects.exclude(id=user.id).exists() is False
|
||||
|
||||
@@ -351,7 +382,7 @@ def test_api_users_update_anonymous():
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
@@ -383,7 +414,7 @@ def test_api_users_update_authenticated_self():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
@@ -409,7 +440,7 @@ def test_api_users_update_authenticated_other():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
for key, value in user_values.items():
|
||||
@@ -431,7 +462,7 @@ def test_api_users_patch_anonymous():
|
||||
{key: new_value},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
@@ -463,7 +494,7 @@ def test_api_users_patch_authenticated_self():
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
@@ -492,7 +523,7 @@ def test_api_users_patch_authenticated_other():
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.status_code == HTTP_403_FORBIDDEN
|
||||
|
||||
user.refresh_from_db()
|
||||
user_values = dict(serializers.UserSerializer(instance=user).data)
|
||||
@@ -507,7 +538,7 @@ def test_api_users_delete_list_anonymous():
|
||||
client = APIClient()
|
||||
response = client.delete("/api/v1.0/users/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert models.User.objects.count() == 2
|
||||
|
||||
|
||||
@@ -522,7 +553,7 @@ def test_api_users_delete_list_authenticated():
|
||||
"/api/v1.0/users/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert models.User.objects.count() == 3
|
||||
|
||||
|
||||
@@ -532,7 +563,7 @@ def test_api_users_delete_anonymous():
|
||||
|
||||
response = APIClient().delete(f"/api/v1.0/users/{user.id!s}/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
|
||||
@@ -548,7 +579,7 @@ def test_api_users_delete_authenticated():
|
||||
f"/api/v1.0/users/{other_user.id!s}/", HTTP_AUTHORIZATION=f"Bearer {jwt_token}"
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert models.User.objects.count() == 2
|
||||
|
||||
|
||||
@@ -562,5 +593,5 @@ def test_api_users_delete_self():
|
||||
HTTP_AUTHORIZATION=f"Bearer {jwt_token}",
|
||||
)
|
||||
|
||||
assert response.status_code == 405
|
||||
assert response.status_code == HTTP_405_METHOD_NOT_ALLOWED
|
||||
assert models.User.objects.count() == 1
|
||||
|
||||
Reference in New Issue
Block a user