Skip to content

Commit 101ef91

Browse files
Merge pull request #38 from RandomProgramm3r/develop
test(user): Add unit tests for user profile, switch primary key to UUID BREAKING CHANGE: changes User.id from integer to UUID. - Cover profile retrieval (GET) and detailed field assertions - Validate error responses for empty names, invalid avatar URLs, and weak passwords - Test full CRUD flow: update name, avatar URL, password change with persistence checks - Ensure legacy JWT remains valid after password update - Verify authentication rejects old password and accepts new password
2 parents cc8a467 + 02a0947 commit 101ef91

File tree

9 files changed

+254
-59
lines changed

9 files changed

+254
-59
lines changed
Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Generated by Django 5.2b1 on 2025-02-28 17:01
1+
# Generated by Django 5.2 on 2025-05-01 19:23
22

3+
import uuid
34
from django.db import migrations, models
45

56

@@ -8,70 +9,66 @@ class Migration(migrations.Migration):
89
initial = True
910

1011
dependencies = [
11-
('auth', '0012_alter_user_first_name_max_length'),
12+
("auth", "0012_alter_user_first_name_max_length"),
1213
]
1314

1415
operations = [
1516
migrations.CreateModel(
16-
name='User',
17+
name="User",
1718
fields=[
19+
("password", models.CharField(max_length=128, verbose_name="password")),
1820
(
19-
'id',
20-
models.BigAutoField(
21-
auto_created=True,
22-
primary_key=True,
23-
serialize=False,
24-
verbose_name='ID',
25-
),
26-
),
27-
(
28-
'password',
29-
models.CharField(max_length=128, verbose_name='password'),
30-
),
31-
(
32-
'is_superuser',
21+
"is_superuser",
3322
models.BooleanField(
3423
default=False,
35-
help_text='Designates that this user has all permissions without explicitly assigning them.',
36-
verbose_name='superuser status',
24+
help_text="Designates that this user has all permissions without explicitly assigning them.",
25+
verbose_name="superuser status",
3726
),
3827
),
39-
('email', models.EmailField(max_length=120, unique=True)),
40-
('name', models.CharField(max_length=100)),
41-
('surname', models.CharField(max_length=120)),
4228
(
43-
'avatar_url',
44-
models.URLField(blank=True, max_length=350, null=True),
29+
"id",
30+
models.UUIDField(
31+
default=uuid.uuid4,
32+
editable=False,
33+
primary_key=True,
34+
serialize=False,
35+
verbose_name="UUID",
36+
),
4537
),
46-
('other', models.JSONField(default=dict)),
47-
('is_active', models.BooleanField(default=True)),
48-
('is_staff', models.BooleanField(default=False)),
49-
('last_login', models.DateTimeField(blank=True, null=True)),
38+
("email", models.EmailField(max_length=120, unique=True)),
39+
("name", models.CharField(max_length=100)),
40+
("surname", models.CharField(max_length=120)),
41+
("avatar_url", models.URLField(blank=True, max_length=350, null=True)),
42+
("other", models.JSONField(default=dict)),
43+
("token_version", models.IntegerField(default=0)),
44+
("is_active", models.BooleanField(default=True)),
45+
("is_staff", models.BooleanField(default=False)),
46+
("last_login", models.DateTimeField(blank=True, null=True)),
5047
(
51-
'groups',
48+
"groups",
5249
models.ManyToManyField(
5350
blank=True,
54-
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
55-
related_name='user_set',
56-
related_query_name='user',
57-
to='auth.group',
58-
verbose_name='groups',
51+
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
52+
related_name="user_set",
53+
related_query_name="user",
54+
to="auth.group",
55+
verbose_name="groups",
5956
),
6057
),
6158
(
62-
'user_permissions',
59+
"user_permissions",
6360
models.ManyToManyField(
6461
blank=True,
65-
help_text='Specific permissions for this user.',
66-
related_name='user_set',
67-
related_query_name='user',
68-
to='auth.permission',
69-
verbose_name='user permissions',
62+
help_text="Specific permissions for this user.",
63+
related_name="user_set",
64+
related_query_name="user",
65+
to="auth.permission",
66+
verbose_name="user permissions",
7067
),
7168
),
7269
],
7370
options={
74-
'abstract': False,
71+
"abstract": False,
7572
},
7673
),
7774
]

promo_code/user/migrations/0002_user_token_version.py

Lines changed: 0 additions & 18 deletions
This file was deleted.

promo_code/user/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import uuid
2+
13
import django.contrib.auth.models
24
import django.db.models
35
import django.utils.timezone
@@ -38,6 +40,12 @@ class User(
3840
django.contrib.auth.models.AbstractBaseUser,
3941
django.contrib.auth.models.PermissionsMixin,
4042
):
43+
id = django.db.models.UUIDField(
44+
'UUID',
45+
primary_key=True,
46+
default=uuid.uuid4,
47+
editable=False,
48+
)
4149
email = django.db.models.EmailField(
4250
unique=True,
4351
max_length=user.constants.EMAIL_MAX_LENGTH,

promo_code/user/tests/auth/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def setUpTestData(cls):
1414
cls.refresh_url = django.urls.reverse('api-user:user-token-refresh')
1515
cls.signup_url = django.urls.reverse('api-user:sign-up')
1616
cls.signin_url = django.urls.reverse('api-user:sign-in')
17+
cls.user_profile_url = django.urls.reverse('api-user:user-profile')
1718

1819
def tearDown(self):
1920
user.models.User.objects.all().delete()

promo_code/user/tests/user/__init__.py

Whitespace-only changes.

promo_code/user/tests/user/operations/__init__.py

Whitespace-only changes.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import rest_framework.status
2+
3+
import user.tests.auth.base
4+
5+
6+
class TestUserProfile(user.tests.auth.base.BaseUserAuthTestCase):
7+
def setUp(self):
8+
super().setUp()
9+
signup_data = {
10+
'name': 'Steve',
11+
'surname': 'Wozniak',
12+
'email': '[email protected]',
13+
'password': 'WhoLivesInCalifornia2000!',
14+
'other': {'age': 23, 'country': 'us'},
15+
}
16+
response = self.client.post(
17+
self.signup_url, signup_data, format='json',
18+
)
19+
token = response.data.get('access')
20+
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)
21+
self.initial_token = token
22+
23+
def test_get_profile_initial(self):
24+
response = self.client.get(self.user_profile_url, format='json')
25+
self.assertEqual(
26+
response.status_code, rest_framework.status.HTTP_200_OK,
27+
)
28+
expected = {
29+
'name': 'Steve',
30+
'surname': 'Wozniak',
31+
'email': '[email protected]',
32+
'other': {'age': 23, 'country': 'us'},
33+
}
34+
self.assertEqual(response.json(), expected)
35+
36+
def test_patch_profile_update_name_and_surname(self):
37+
payload = {'name': 'John', 'surname': 'Tsal'}
38+
response = self.client.patch(
39+
self.user_profile_url, payload, format='json',
40+
)
41+
self.assertEqual(
42+
response.status_code, rest_framework.status.HTTP_200_OK,
43+
)
44+
self.assertEqual(response.data.get('name'), 'John')
45+
self.assertEqual(response.data.get('surname'), 'Tsal')
46+
47+
def test_patch_profile_update_avatar_url(self):
48+
payload = {'avatar_url': 'http://nodomain.com/kitten.jpeg'}
49+
response = self.client.patch(
50+
self.user_profile_url, payload, format='json',
51+
)
52+
self.assertEqual(
53+
response.status_code, rest_framework.status.HTTP_200_OK,
54+
)
55+
self.assertEqual(
56+
response.data.get('avatar_url'), 'http://nodomain.com/kitten.jpeg',
57+
)
58+
59+
def test_patch_password_and_check_persistence(self):
60+
new_password = 'MegaGiant88888@dooRuveS'
61+
self.client.patch(
62+
self.user_profile_url,
63+
{'name': 'John', 'surname': 'Tsal'},
64+
format='json',
65+
)
66+
self.client.patch(
67+
self.user_profile_url,
68+
{'avatar_url': 'http://nodomain.com/kitten.jpeg'},
69+
format='json',
70+
)
71+
response = self.client.patch(
72+
self.user_profile_url, {'password': new_password}, format='json',
73+
)
74+
self.assertEqual(
75+
response.status_code, rest_framework.status.HTTP_200_OK,
76+
)
77+
data = response.data
78+
self.assertEqual(data.get('name'), 'John')
79+
self.assertEqual(data.get('surname'), 'Tsal')
80+
self.assertEqual(data.get('email'), '[email protected]')
81+
self.assertEqual(data.get('other'), {'age': 23, 'country': 'us'})
82+
self.assertEqual(
83+
data.get('avatar_url'), 'http://nodomain.com/kitten.jpeg',
84+
)
85+
86+
# test old token still valid
87+
response = self.client.get(self.user_profile_url, format='json')
88+
self.assertEqual(
89+
response.status_code, rest_framework.status.HTTP_200_OK,
90+
)
91+
92+
def test_auth_sign_in_old_password_fails(self):
93+
new_password = 'MegaGiant88888@dooRuveS'
94+
response = self.client.patch(
95+
self.user_profile_url, {'password': new_password}, format='json',
96+
)
97+
self.client.credentials()
98+
response = self.client.post(
99+
self.signin_url,
100+
{
101+
'email': '[email protected]',
102+
'password': 'WhoLivesInCalifornia2000!',
103+
},
104+
format='json',
105+
)
106+
self.assertEqual(
107+
response.status_code, rest_framework.status.HTTP_401_UNAUTHORIZED,
108+
)
109+
110+
def test_auth_sign_in_new_password_succeeds(self):
111+
new_password = 'MegaGiant88888@dooRuveS'
112+
response = self.client.patch(
113+
self.user_profile_url, {'password': new_password}, format='json',
114+
)
115+
self.client.credentials()
116+
response = self.client.post(
117+
self.signin_url,
118+
{
119+
'email': '[email protected]',
120+
'password': 'MegaGiant88888@dooRuveS',
121+
},
122+
format='json',
123+
)
124+
self.assertEqual(
125+
response.status_code, rest_framework.status.HTTP_200_OK,
126+
)

promo_code/user/tests/user/validations/__init__.py

Whitespace-only changes.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import parameterized
2+
import rest_framework.status
3+
4+
import user.tests.auth.base
5+
6+
7+
class ProfileAPITestCase(user.tests.auth.base.BaseUserAuthTestCase):
8+
def setUp(self):
9+
super().setUp()
10+
signup_data = {
11+
'name': 'Jack',
12+
'surname': 'Sparrow',
13+
'email': '[email protected]',
14+
'password': 'WhoLivesInTheOcean100500!',
15+
'other': {'age': 48, 'country': 'gb'},
16+
}
17+
response = self.client.post(
18+
self.signup_url, signup_data, format='json',
19+
)
20+
token = response.data.get('access')
21+
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + token)
22+
23+
def test_update_profile_empty_name_and_surname(self):
24+
payload = {'name': '', 'surname': ''}
25+
response = self.client.patch(
26+
self.user_profile_url, payload, format='json',
27+
)
28+
self.assertEqual(
29+
response.status_code, rest_framework.status.HTTP_400_BAD_REQUEST,
30+
)
31+
32+
@parameterized.parameterized.expand(
33+
[
34+
('no_scheme', 'notURLcom'),
35+
('only_scheme', 'https://'),
36+
('no_domain', 'https://.com'),
37+
],
38+
)
39+
def test_update_profile_invalid_avatar_url(self, name, url):
40+
payload = {'avatar_url': url}
41+
response = self.client.patch(
42+
self.user_profile_url, payload, format='json',
43+
)
44+
self.assertEqual(
45+
response.status_code, rest_framework.status.HTTP_400_BAD_REQUEST,
46+
)
47+
48+
@parameterized.parameterized.expand(
49+
[
50+
('simple_alpha_num', 'pro100'),
51+
('only_symbols', '!!!!!'),
52+
('only_nums', '1234567890'),
53+
('only_lowercase_chars', 'abcdefghijklmno'),
54+
('only_uppercase-chars', 'ABCDEFGHIJKLMNO'),
55+
('only_symbols_and_nums', '1234567890!@#$%^&*()_+{}|:"<>?'),
56+
('repetitive_chars', 'onlyYOUOOOO!'),
57+
('mixed_short', 'yOu!@1'),
58+
('repeating_pattern', '11111@@@@@aaaaa'),
59+
],
60+
)
61+
def test_update_profile_weak_password(self, name, pwd):
62+
payload = {'password': pwd}
63+
response = self.client.patch(
64+
self.user_profile_url, payload, format='json',
65+
)
66+
self.assertEqual(
67+
response.status_code, rest_framework.status.HTTP_400_BAD_REQUEST,
68+
)
69+
70+
def test_get_profile(self):
71+
response = self.client.get(self.user_profile_url, format='json')
72+
self.assertEqual(
73+
response.status_code, rest_framework.status.HTTP_200_OK,
74+
)
75+
expected = {
76+
'name': 'Jack',
77+
'surname': 'Sparrow',
78+
'email': '[email protected]',
79+
'other': {'age': 48, 'country': 'gb'},
80+
}
81+
self.assertEqual(response.json(), expected)

0 commit comments

Comments
 (0)