I'm quite new to factory-boy and I'm trying to send a request to an API endpoint in my unit test, which requires a user to be authenticated. The endpoint expects a token in the header in the form of 'Bearer ' + token. I've looked at a few examples online and this is what I've come up with so far in my unit test:
test_user.py
class UserFactory(factory.Factory):
class Meta:
model = user
username = factory.LazyAttribute(lambda t: "myuser")
password = factory.PostGenerationMethodCall('set_password', 'my_super_secret')
is_staff = True
is_active = True
class UserViewSetTest(TestCase):
def setUp(self):
pwd = 'my_super_secret'
self.user = UserFactory(password=pwd)
self.client = Client()
self.assertTrue(self.client.login(username=self.user.username, password=pwd))
def test_user_list(self):
response = self.client.get(reverse('user', kwargs={'fromdate': '2017-01-01', 'todate': '2017-04-01'})), format='json')
self.assertEqual(response.status_code, 200)
The initial error is the fact that this assertion self.assertTrue(self.client.login(username=self.user.username, password=pwd)) is false so the test fails right away. Even if I remove that line, the API call returns a 401 because the authentication isn't successful.
How can I successfully authenticate a user in this API call with factory-boy so that I can send a token in the API request? Can I use a user model provided by the framework?
EDIT:
I've tried to create a token in order to pass it through the header as such:
def setUp(self):
self.user = UserFactory.create()
self.factory = APIRequestFactory()
self.token = Token.objects.create(user=self.user)
self.token.save()
def test_user_list(self):
self.client = APIClient()
self.client.credentials(HTTP-AUTHORIZATION='Bearer ' + self.token.key)
response = self.client.get(reverse('user', kwargs={'fromdate': '2017-01-01', 'todate': '2017-04-01'})), format='json')
self.assertEqual(response.status_code, 200)
However, I'm still getting an
AssertionError: 401 != 200
I've also seen that there is a force_authentication method but I'm not sure how to use it. Any help is appreciated.
You're using factory.Factory instead of factory.django.DjangoModelFactory.
factory.Factory doesn't automatically save to the db, so you can either switch to DjangoModelFactory, or run self.user.save() manually
You also don't need self.client = Client(), as self.client already exists
I have forced the authentication in the following way. I'm not sure this is the best practice as it seems to be bypassing the issue rather than resolving it, so if anyone else has other ideas, please let me know.
view = views.{VIEWSET_ENDPOINT}.as_view({'get': 'list'})
request = self.factory.get('{ENDPOINT}')
force_authenticate(request, user=self.user)
response = view(request)
Related
I have the following code that sends requests to check JWT token, then authorize user and return authorized session with Access token, Refresh Token and Session ID.
#csrf_exempt
def new_login_view(request, *args, **kwargs):
def convert_data(req):
data = {
"email": req.data['username'],
"password": req.data['password'],
}
try:
data["language"] = request.LANGUAGE_CODE
except:
data["language"] = request.POST.get('language', 'en')
return data
if request.user.is_authenticated and not request.META.get('HTTP_X_AVOID_COOKIES'):
return HttpResponseRedirect(request.GET.get(KEY_NEXT, '/'))
if request.method == 'POST':
request_data = convert_data(request)
# request to Accounts API to check if user exists
response = send_service_request(EnumAuthUrls.sign_in.value,
json_data=request_data,
original_response=True)
if isinstance(response, dict):
return JsonResponse(response)
if response.status_code == 200:
tok_ac = response.headers.get(HEADER_ACCESS_KEY)
tok_ref = response.headers.get(HEADER_REFRESH_KEY)
# checking JWT token
user = ApiAuthenticationBackend().authenticate(request, tok_ac)
# creates session
data = login_session(request, response, user)
data['user_id'] = request.user.id
data['account_id'] = request.user.profile.account_id
data['balance'] = request.user.balance
if request.META.get('HTTP_X_AVOID_COOKIES'):
return JsonResponse(data)
response = AuthResponse(
data=data,
ssid=request.session.session_key,
access_token=tok_ac,
refresh_token=tok_ref,
)
return response
else:
return ErrorApiResponse(response.json())
service = urllib.parse.quote_plus(request.build_absolute_uri())
return HttpResponseRedirect(settings.ACCOUNTS_URL + f'login/?service={service}')
Here's the code of login_session fucntion:
def login_session(request: HttpRequest, response: HttpResponse, user):
request.user = user
request.session.create()
base_data = response.json().get(KEY_DATA)
return request.user.serialize(request, base_data, token=True)
And here's the class AuthResponse that is eventually based on HttpResponse:
class AuthResponse(SuccessResponse):
def __init__(self, data={}, ssid='', access_token: str = '', refresh_token: str = '', **kwargs):
super().__init__(data, **kwargs)
if ssid:
logger.debug(f'Setting {settings.SESSION_COOKIE_NAME}: {ssid}')
self.set_cookie(key=settings.SESSION_COOKIE_NAME,
value=ssid)
if access_token:
self.set_cookie(key=settings.ACCESS_KEY_COOKIE_NAME,
value=access_token)
if refresh_token:
self.set_cookie(key=settings.REFRESH_KEY_COOKIE_NAME,
value=refresh_token)
The problem is that looks everything good on the browser side, I get all needed cookies (access token, refresh token and session id) however after trying logging in I get redirected to the main page.
There was problem in the beginning with setting cookies, but then I found out that I should not use SESSION_COOKIE_DOMAIN if it's local. Thus all cookies came up without problem but it didn't resolve situation with authorization.
While setting cookies with self.set_cookie() I tried to use secure=True, samesite='Lax', httponly=True, and all other parameters but it didn't help.
Does anyone knows what else I can try in order to fix it?
Well, I found what was wrong!
There was middleware that supposed to check token from another service. However it was checking old token, instead of new one.
So once I changed it and started to check new token - it was working just fine.
So if there's no other solutions, make sure you have checked middleware or other code where it could affect on whole system.
I have added a method to my viewset as follows:
class CustomImageViewSet(viewsets.ModelViewSet):
queryset = CustomImage.objects.all()
serializer_class = CustomImageSerializer
lookup_field = 'id'
#action(detail=True, methods=['get'], url_path='sepia/')
def sepia(self, request, id):
# do something
data = image_to_string(image)
return HttpResponse(data, content_type="image/png", status=status.HTTP_200_OK)
Since it is not a default or overridden request method, I am not sure how can I proceed writing a test for it. Any suggestions?
You're not clear on what the test should test but you can test the response status_code for example like this:
def test_sepia_api():
api_client = APIClient()
response = api_client.get(path="{path_to_your_api}/sepia/")
assert response.status_code == 200
I noticed you were using pytest. I'll assume you've got pytest-django too then (it really does make everything easier). I like using request factory since it's generally faster if you've got authentication needs.
def test_me(self, user, rf):
view = CustomImageViewSet()
request = rf.get("")
request.user = user # If you need authentication
view.request = request
response = view.sepia(request, 123)
assert response.data == BLAH
I'm new to coding and I feel really stuck. I decided to unit test my micro Flask app. I use "Google sign in" to authenticate my users, but I'm not able to get through the Google authentication and assert what is really on the page. I have no idea if I need to mock, play with the session or application context.
Here is the code for authentication process:
def login_required(f):
#wraps(f)
def decorated_function(*args, **kwargs):
user = dict(session).get('profile', None)
if user is not None:
email = session['profile']['email']
else:
email = None
if user:
return f(*args, **kwargs)
return render_template('login.html', email=email)
return decorated_function
#app.route('/login/')
def login():
google = oauth.create_client('google') # create the google oauth client
redirect_uri = url_for('authorize', _external=True)
return google.authorize_redirect(redirect_uri)
#app.route('/authorize')
def authorize():
google = oauth.create_client('google') # create the google oauth client
token = google.authorize_access_token() # Access token from google (needed to get user info)
resp = google.get('userinfo') # userinfo specificed in the scrope
user_info = resp.json()
# Loading and cleaning all user emails
auth_users = db.session.query(users.email).all()
auth_users = [str(i).strip("(),'") for i in auth_users]
# Checking if user is in the list
if user_info['email'] in auth_users:
flash("Boli ste úspešne prihlásený.", category="success")
else:
flash("Nemáte práva na prístup.", category="primary")
return redirect('/')
session['profile'] = user_info
# make the session pernament so it keeps existing after broweser gets closed
session.permanent = True
return redirect('/')
The test structure look like this:
class Uvo_page(unittest.TestCase):
def setUp(self):
self.client = app.test_client(self)
self.user_info = {'email': 'xxx'}
self.auth_users = ['xxx', 'yyy']
# UVO page shows results
def test_uvo_page_show_results(self):
response = self.client.get('/show/1',
content_type='html/text',
follow_redirects=True)
test_string = bytes('Sledované stránky', 'utf-8')
self.assertTrue(test_string in response.data)
I thought that by defining the "user_info" it will be used for authentication evaluation. Instead of it it does not continue to "/authorize" and authentication, but stays on the login page.
I need to test my API. For example, I have images list page. I need to do tests for this page, but I cannot do this without authentication. I use JWT. I do not know how to do it.
Help me please.
tests.py
class ImagesListTestCase(APITestCase):
def test_images_list(self):
response = self.client.get('/api/', HTTP_AUTHORIZATION="JWT {}".format("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTkzNzUxMzMxLCJqdGkiOiI3NWE3NDNkMGU3MDQ0MGNiYjQ3NDExNjQ3MTI5NWVjNSIsInVzZXJfaWQiOjF9.7HkhZ1hRV8OtQJMMLEAVwUnJ0yDt8agFadAsJztFb6A"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
I tried to do
response = self.client.get('/api/', Authorization="Bearer <token>")
Also
response = self.client.get('/api/', Authorization="JWT <token>")
response = self.client.get('/api/', HTTP_AUTHORIZATION="Bearer <token>")
django creates a temporary database for test; so it's better to get token from username and password like this:
class ImagesListTestCase(APITestCase):
def setUp(self) :
self.register_url = reverse("your:register:view") # for example : "users:register"
self.user_data = {
"username": "test_user",
"email": "test_user#gmail.com",
"password": "123456"
}
self.client.post(self.register_url,self.user_data) # user created
auth_url = reverse("your:login:view") #for example :"users:token_obtain_pair"
self.access_token = self.client.post(auth_url,{
"username" : self.user_data.get("username") ,
"password" : self.user_data.get("password")
}).data.get("access") # get access_token for authorization
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}')
def test_images_list(self):
response = self.client.get('/api/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
You can create users that require JWT authentication like:
def setUp(self) -> None:
self.client = APIClient()
if os.environ.get('GITHUB_WORKFLOW'):
local_token_url = 'https://testing/app/token/jwt'
else:
local_token_url = 'http://127.0.0.1:8000/app/token/jwt'
response = requests.post(local_token_url, {'email': 'test_contact1#user.com', 'password': '123pass321'})
self.test_user1 = json.loads(response.text)
response = requests.post(local_token_url, {'email': 'test_contact2#user.com', 'password': '123pass321'})
self.test_user2 = json.loads(response.text)
self.contact_person = apm.AppUser.objects.create(email="test_contact1#user.com", first_name="John",
last_name="Doe",
company_name="test_company_1", phone_number="08077745673",
is_active=True)
you can then use them like below parsing data = {}
self.client.credentials(HTTP_AUTHORIZATION="Bearer {}".format(self.test_user1.get('access')))
response = self.client.post(reverse('create-list'), data=data, format='json')
print(response.data)
self.assertEqual(response.status_code, status.HTTP_201_CREATE
Instead of invoking api to get the token, Another very simple solution can as follows
from rest_framework_simplejwt.tokens import AccessToken
def setUp(self):
self.factory = APIRequestFactory()
user = User.objects.create_user(
username='jacob', email='jacob#gmail.com', password='top_secret')
self.client = APIClient()
token = AccessToken.for_user(user=user)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
and it will be used as
def test_get_cities(self):
response = self.client.get('/<actual path to url>/cities/')
assert response.status_code == 200
assert len(response.data) == 0
self.assertEqual(response.data, [])
Important thing to notice here is in setUp function token is generated using AcessToken class from JWT itself, so no actual API is required for testing other flows in your app.
Perhaps I am missing the logic but I want to add two unique records to a model then run my tests on an API. But my API complains .get() has returned 2 records, raising the error. I notice the error happens on my .post() check, while the get methods work well. Is it a bug?
only enough if I change self.client.post with .get(), it doesn't complain.
I have tried setUpTestData() since my database supports Transactions to no avail. Essentially, I want to try the get method and the post methods. All the get operations pass but on the post test, it fails.
class ResetPwdTest(APITestCase):
""" Test module for resetting forgotten password """
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.valid_payload = '1234567890ABCDEF'
cls.expired_payload = '1234567ACDES'
cls.invalid_payload = 'invalid0000'
user_info = {'first_name': 'John', 'email': 'kmsium#gmail.com','password' : 'kigali11', 'status': 1}
user = User.objects.create_user(**user_info)
PasswordReset.objects.all().delete() # delete any possible we have but having no effect so far
# create valid password token
password2 = PasswordReset(profile_id=user.id, unique_code=cls.valid_payload)
password2.save()
# create an expired passwod token
password = PasswordReset(profile_id=user.id, unique_code=cls.expired_payload)
password.save()
password.createdon = '2018-01-12 21:11:38.997207'
password.save()
def test_valid_token(self):
"""
GET Ensure a valid forgotten password reset token returns 200
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.valid_payload}))
self.assertEqual(response.status_code, 200)
def test_expired_token(self):
"""
GET Ensure an expired token to reset a forgotten password returns 400
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.expired_payload}))
self.assertEqual(response.status_code, 400)
def test_invalid_token(self):
"""
GET Ensure an invali token to reset a forgotten password returns 400
WORKING
"""
response = self.client.get(reverse('new-pwd', kwargs= {'token' : self.invalid_payload}))
self.assertEqual(response.status_code, 400)
def test_valid_token_change_pass(self):
"""
POST Ensure a valid forgotten password with accepted passwords pass reset token returns 200
FAILING BECAUSE TOKEN is not unique
"""
passwords = {'pwd': 'letmein11', 'pwd_confirm': 'letmein11'}
response = self.client.post(reverse('new-pwd', kwargs= {'token' : self.valid_payload}), passwords, format='json')
self.assertEqual(response.status_code, 200)
the View:
class ResetPwd(APIView):
permission_classes = []
def get(self,request,token,format=None):
status = 400
reply = {}
check_token = PasswordReset.objects.values('createdon','profile_id',).get(unique_code=token)
# exists but is it valid or not? It cant be
createdon = check_token["createdon"]
if createdon > timezone.now() - timedelta(hours=USER_VALUES['PWD_RESET_LIFESPAN']):
status = 200
reply['detail'] = True
else:
reply['detail'] = _('errorPwdResetLinkExpired')
return JsonResponse(reply,status=status)
def post(self,request,token,format=None):
'''
#input pwd: password
#input pwd_confirm : confirmation password
'''
status = 400
reply = {}
k = PasswordReset.objects.filter(unique_code= token).count()
print('total in db ', k) # shows 1
check_token = PasswordReset.objects.values('createdon','profile_id',).get(unique_code=token)
# error: returning 2!
#exists but is it valid or not? It cant be
createdon = check_token['createdon']
if createdon > timezone.now() - timedelta(hours=USER_VALUES['PWD_RESET_LIFESPAN']):
status = 200
else:
reply['detail'] = _('errorPwdResetLinkExpired')
'''
except:
reply['detail'] = _('errorBadPwdResetLink')
'''
return JsonResponse(reply,status=status)
I expect all the tests to pass.