authentik.core.tests.test_users_api
Test Users API
1"""Test Users API""" 2 3from datetime import datetime, timedelta 4from json import loads 5 6from django.urls.base import reverse 7from django.utils.timezone import now 8from rest_framework.test import APITestCase 9 10from authentik.brands.models import Brand 11from authentik.core.models import ( 12 USER_ATTRIBUTE_TOKEN_EXPIRING, 13 AuthenticatedSession, 14 Session, 15 Token, 16 User, 17 UserTypes, 18) 19from authentik.core.tests.utils import ( 20 create_test_admin_user, 21 create_test_brand, 22 create_test_flow, 23 create_test_user, 24) 25from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation 26from authentik.lib.generators import generate_id, generate_key 27from authentik.stages.email.models import EmailStage 28 29 30class TestUsersAPI(APITestCase): 31 """Test Users API""" 32 33 def setUp(self) -> None: 34 self.admin = create_test_admin_user() 35 self.user = create_test_user() 36 37 def test_filter_type(self): 38 """Test API filtering by type""" 39 self.client.force_login(self.admin) 40 user = create_test_admin_user(type=UserTypes.EXTERNAL) 41 response = self.client.get( 42 reverse("authentik_api:user-list"), 43 data={ 44 "type": UserTypes.EXTERNAL, 45 "username": user.username, 46 }, 47 ) 48 self.assertEqual(response.status_code, 200) 49 50 def test_filter_is_superuser(self): 51 """Test API filtering by superuser status""" 52 User.objects.all().delete() 53 admin = create_test_admin_user() 54 self.client.force_login(admin) 55 # Test superuser 56 response = self.client.get( 57 reverse("authentik_api:user-list"), 58 data={ 59 "is_superuser": True, 60 }, 61 ) 62 self.assertEqual(response.status_code, 200) 63 body = loads(response.content) 64 self.assertEqual(len(body["results"]), 1) 65 self.assertEqual(body["results"][0]["username"], admin.username) 66 # Test non-superuser 67 user = create_test_user() 68 response = self.client.get( 69 reverse("authentik_api:user-list"), 70 data={ 71 "is_superuser": False, 72 }, 73 ) 74 self.assertEqual(response.status_code, 200) 75 body = loads(response.content) 76 self.assertEqual(len(body["results"]), 1, body) 77 self.assertEqual(body["results"][0]["username"], user.username) 78 79 def test_list_with_groups(self): 80 """Test listing with groups""" 81 self.client.force_login(self.admin) 82 response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"}) 83 self.assertEqual(response.status_code, 200) 84 85 def test_recovery_no_flow(self): 86 """Test user recovery link (no recovery flow set)""" 87 self.client.force_login(self.admin) 88 response = self.client.post( 89 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 90 ) 91 self.assertEqual(response.status_code, 400) 92 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."}) 93 94 def test_set_password(self): 95 """Test Direct password set""" 96 self.client.force_login(self.admin) 97 new_pw = generate_key() 98 response = self.client.post( 99 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 100 data={"password": new_pw}, 101 ) 102 self.assertEqual(response.status_code, 204) 103 self.admin.refresh_from_db() 104 self.assertTrue(self.admin.check_password(new_pw)) 105 106 def test_set_password_blank(self): 107 """Test Direct password set""" 108 self.client.force_login(self.admin) 109 response = self.client.post( 110 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 111 data={"password": ""}, 112 ) 113 self.assertEqual(response.status_code, 400) 114 self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]}) 115 116 def test_recovery(self): 117 """Test user recovery link""" 118 flow = create_test_flow( 119 FlowDesignation.RECOVERY, 120 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 121 ) 122 brand: Brand = create_test_brand() 123 brand.flow_recovery = flow 124 brand.save() 125 self.client.force_login(self.admin) 126 response = self.client.post( 127 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 128 ) 129 self.assertEqual(response.status_code, 200) 130 131 def test_recovery_duration(self): 132 """Test user recovery token duration""" 133 Token.objects.all().delete() 134 flow = create_test_flow( 135 FlowDesignation.RECOVERY, 136 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 137 ) 138 brand: Brand = create_test_brand() 139 brand.flow_recovery = flow 140 brand.save() 141 self.client.force_login(self.admin) 142 response = self.client.post( 143 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 144 data={"token_duration": "days=33"}, 145 ) 146 self.assertEqual(response.status_code, 200) 147 expires = Token.objects.first().expires 148 expected_expires = now() + timedelta(days=33) 149 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 150 151 def test_recovery_duration_update(self): 152 """Test user recovery token duration update""" 153 Token.objects.all().delete() 154 flow = create_test_flow( 155 FlowDesignation.RECOVERY, 156 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 157 ) 158 brand: Brand = create_test_brand() 159 brand.flow_recovery = flow 160 brand.save() 161 self.client.force_login(self.admin) 162 response = self.client.post( 163 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 164 data={"token_duration": "days=33"}, 165 ) 166 self.assertEqual(response.status_code, 200) 167 expires = Token.objects.first().expires 168 expected_expires = now() + timedelta(days=33) 169 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 170 response = self.client.post( 171 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 172 data={"token_duration": "days=66"}, 173 ) 174 expires = Token.objects.first().expires 175 expected_expires = now() + timedelta(days=66) 176 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 177 178 def test_recovery_email_no_flow(self): 179 """Test user recovery link (no recovery flow set)""" 180 self.client.force_login(self.admin) 181 self.user.email = "" 182 self.user.save() 183 stage = EmailStage.objects.create(name="email") 184 response = self.client.post( 185 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 186 data={"email_stage": stage.pk}, 187 ) 188 self.assertEqual(response.status_code, 400) 189 self.assertJSONEqual( 190 response.content, {"non_field_errors": "User does not have an email address set."} 191 ) 192 self.user.email = "foo@bar.baz" 193 self.user.save() 194 response = self.client.post( 195 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 196 data={"email_stage": stage.pk}, 197 ) 198 self.assertEqual(response.status_code, 400) 199 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."}) 200 201 def test_recovery_email_no_stage(self): 202 """Test user recovery link (no email stage)""" 203 self.user.email = "foo@bar.baz" 204 self.user.save() 205 flow = create_test_flow(designation=FlowDesignation.RECOVERY) 206 brand: Brand = create_test_brand() 207 brand.flow_recovery = flow 208 brand.save() 209 self.client.force_login(self.admin) 210 response = self.client.post( 211 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) 212 ) 213 self.assertEqual(response.status_code, 400) 214 self.assertJSONEqual(response.content, {"email_stage": ["This field is required."]}) 215 216 def test_recovery_email(self): 217 """Test user recovery link""" 218 self.user.email = "foo@bar.baz" 219 self.user.save() 220 flow = create_test_flow(FlowDesignation.RECOVERY) 221 brand: Brand = create_test_brand() 222 brand.flow_recovery = flow 223 brand.save() 224 225 stage = EmailStage.objects.create(name="email") 226 227 self.client.force_login(self.admin) 228 response = self.client.post( 229 reverse( 230 "authentik_api:user-recovery-email", 231 kwargs={"pk": self.user.pk}, 232 ), 233 data={"email_stage": stage.pk}, 234 ) 235 self.assertEqual(response.status_code, 204) 236 237 def test_service_account(self): 238 """Service account creation""" 239 self.client.force_login(self.admin) 240 response = self.client.post(reverse("authentik_api:user-service-account")) 241 self.assertEqual(response.status_code, 400) 242 response = self.client.post( 243 reverse("authentik_api:user-service-account"), 244 data={ 245 "name": "test-sa", 246 "create_group": True, 247 }, 248 ) 249 self.assertEqual(response.status_code, 200) 250 251 user_filter = User.objects.filter( 252 username="test-sa", 253 type=UserTypes.SERVICE_ACCOUNT, 254 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 255 ) 256 self.assertTrue(user_filter.exists()) 257 user: User = user_filter.first() 258 self.assertFalse(user.has_usable_password()) 259 260 token_filter = Token.objects.filter(user=user) 261 self.assertTrue(token_filter.exists()) 262 self.assertTrue(token_filter.first().expiring) 263 264 def test_service_account_no_expire(self): 265 """Service account creation without token expiration""" 266 self.client.force_login(self.admin) 267 response = self.client.post( 268 reverse("authentik_api:user-service-account"), 269 data={ 270 "name": "test-sa", 271 "create_group": True, 272 "expiring": False, 273 }, 274 ) 275 self.assertEqual(response.status_code, 200) 276 277 user_filter = User.objects.filter( 278 username="test-sa", 279 type=UserTypes.SERVICE_ACCOUNT, 280 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False}, 281 ) 282 self.assertTrue(user_filter.exists()) 283 user: User = user_filter.first() 284 self.assertFalse(user.has_usable_password()) 285 286 token_filter = Token.objects.filter(user=user) 287 self.assertTrue(token_filter.exists()) 288 self.assertFalse(token_filter.first().expiring) 289 290 def test_service_account_with_custom_expire(self): 291 """Service account creation with custom token expiration date""" 292 self.client.force_login(self.admin) 293 expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone() 294 response = self.client.post( 295 reverse("authentik_api:user-service-account"), 296 data={ 297 "name": "test-sa", 298 "create_group": True, 299 "expires": expire_on.isoformat(), 300 }, 301 ) 302 self.assertEqual(response.status_code, 200) 303 304 user_filter = User.objects.filter( 305 username="test-sa", 306 type=UserTypes.SERVICE_ACCOUNT, 307 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 308 ) 309 self.assertTrue(user_filter.exists()) 310 user: User = user_filter.first() 311 self.assertFalse(user.has_usable_password()) 312 313 token_filter = Token.objects.filter(user=user) 314 self.assertTrue(token_filter.exists()) 315 token = token_filter.first() 316 self.assertTrue(token.expiring) 317 self.assertEqual(token.expires, expire_on) 318 319 def test_service_account_invalid(self): 320 """Service account creation (twice with same name, expect error)""" 321 self.client.force_login(self.admin) 322 response = self.client.post( 323 reverse("authentik_api:user-service-account"), 324 data={ 325 "name": "test-sa", 326 "create_group": True, 327 }, 328 ) 329 self.assertEqual(response.status_code, 200) 330 331 user_filter = User.objects.filter( 332 username="test-sa", 333 type=UserTypes.SERVICE_ACCOUNT, 334 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 335 ) 336 self.assertTrue(user_filter.exists()) 337 user: User = user_filter.first() 338 self.assertFalse(user.has_usable_password()) 339 340 token_filter = Token.objects.filter(user=user) 341 self.assertTrue(token_filter.exists()) 342 self.assertTrue(token_filter.first().expiring) 343 344 response = self.client.post( 345 reverse("authentik_api:user-service-account"), 346 data={ 347 "name": "test-sa", 348 "create_group": True, 349 }, 350 ) 351 self.assertEqual(response.status_code, 400) 352 353 def test_paths(self): 354 """Test path""" 355 self.client.force_login(self.admin) 356 response = self.client.get( 357 reverse("authentik_api:user-paths"), 358 ) 359 self.assertEqual(response.status_code, 200) 360 expected = list( 361 User.objects.all() 362 .values("path") 363 .distinct() 364 .order_by("path") 365 .values_list("path", flat=True) 366 ) 367 self.assertJSONEqual(response.content.decode(), {"paths": expected}) 368 369 def test_path_valid(self): 370 """Test path""" 371 self.client.force_login(self.admin) 372 response = self.client.post( 373 reverse("authentik_api:user-list"), 374 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo"}, 375 ) 376 self.assertEqual(response.status_code, 201) 377 378 def test_path_invalid(self): 379 """Test path (invalid)""" 380 self.client.force_login(self.admin) 381 response = self.client.post( 382 reverse("authentik_api:user-list"), 383 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "/foo"}, 384 ) 385 self.assertEqual(response.status_code, 400) 386 self.assertJSONEqual( 387 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 388 ) 389 390 self.client.force_login(self.admin) 391 response = self.client.post( 392 reverse("authentik_api:user-list"), 393 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": ""}, 394 ) 395 self.assertEqual(response.status_code, 400) 396 self.assertJSONEqual(response.content.decode(), {"path": ["This field may not be blank."]}) 397 398 response = self.client.post( 399 reverse("authentik_api:user-list"), 400 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo/"}, 401 ) 402 self.assertEqual(response.status_code, 400) 403 self.assertJSONEqual( 404 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 405 ) 406 407 response = self.client.post( 408 reverse("authentik_api:user-list"), 409 data={ 410 "name": generate_id(), 411 "username": generate_id(), 412 "groups": [], 413 "path": "fos//o", 414 }, 415 ) 416 self.assertEqual(response.status_code, 400) 417 self.assertJSONEqual( 418 response.content.decode(), {"path": ["No empty segments in user path allowed."]} 419 ) 420 421 def test_me(self): 422 """Test user's me endpoint""" 423 self.client.force_login(self.admin) 424 response = self.client.get(reverse("authentik_api:user-me")) 425 self.assertEqual(response.status_code, 200) 426 427 def test_session_delete(self): 428 """Ensure sessions are deleted when a user is deactivated""" 429 user = create_test_admin_user() 430 session_id = generate_id() 431 session = Session.objects.create( 432 session_key=session_id, 433 last_ip="255.255.255.255", 434 last_user_agent="", 435 ) 436 AuthenticatedSession.objects.create( 437 session=session, 438 user=user, 439 ) 440 441 self.client.force_login(self.admin) 442 response = self.client.patch( 443 reverse("authentik_api:user-detail", kwargs={"pk": user.pk}), 444 data={ 445 "is_active": False, 446 }, 447 ) 448 self.assertEqual(response.status_code, 200) 449 450 self.assertFalse(Session.objects.filter(session_key=session_id).exists()) 451 self.assertFalse( 452 AuthenticatedSession.objects.filter(session__session_key=session_id).exists() 453 ) 454 455 def test_sort_by_last_updated(self): 456 """Test API sorting by last_updated""" 457 User.objects.all().delete() 458 admin = create_test_admin_user() 459 self.client.force_login(admin) 460 461 user = create_test_user() 462 admin.first_name = "Sample change" 463 admin.last_name = "To trigger an update" 464 admin.save() 465 466 # Ascending 467 response = self.client.get( 468 reverse("authentik_api:user-list"), 469 data={ 470 "ordering": "last_updated", 471 }, 472 ) 473 self.assertEqual(response.status_code, 200) 474 475 body = loads(response.content) 476 self.assertEqual(len(body["results"]), 2) 477 self.assertEqual(body["results"][0]["pk"], user.pk) 478 479 # Descending 480 response = self.client.get( 481 reverse("authentik_api:user-list"), 482 data={ 483 "ordering": "-last_updated", 484 }, 485 ) 486 self.assertEqual(response.status_code, 200) 487 488 body = loads(response.content) 489 self.assertEqual(len(body["results"]), 2) 490 self.assertEqual(body["results"][0]["pk"], admin.pk) 491 492 def test_sort_by_date_joined(self): 493 """Test API sorting by date_joined""" 494 User.objects.all().delete() 495 admin = create_test_admin_user() 496 self.client.force_login(admin) 497 498 user = create_test_user() 499 500 response = self.client.get( 501 reverse("authentik_api:user-list"), 502 data={ 503 "ordering": "date_joined", 504 }, 505 ) 506 self.assertEqual(response.status_code, 200) 507 508 body = loads(response.content) 509 self.assertEqual(len(body["results"]), 2) 510 self.assertEqual(body["results"][0]["pk"], admin.pk) 511 512 response = self.client.get( 513 reverse("authentik_api:user-list"), 514 data={ 515 "ordering": "-date_joined", 516 }, 517 ) 518 self.assertEqual(response.status_code, 200) 519 520 body = loads(response.content) 521 self.assertEqual(len(body["results"]), 2) 522 self.assertEqual(body["results"][0]["pk"], user.pk) 523 524 def test_service_account_validation_empty_username(self): 525 """Test service account creation with empty/blank username validation""" 526 self.client.force_login(self.admin) 527 528 # Test with empty string 529 response = self.client.post( 530 reverse("authentik_api:user-service-account"), 531 data={ 532 "name": "", 533 "create_group": True, 534 }, 535 ) 536 self.assertEqual(response.status_code, 400) 537 self.assertJSONEqual( 538 response.content, 539 {"name": ["This field may not be blank."]}, 540 ) 541 542 # Test with only whitespace 543 response = self.client.post( 544 reverse("authentik_api:user-service-account"), 545 data={ 546 "name": " ", 547 "create_group": True, 548 }, 549 ) 550 self.assertEqual(response.status_code, 400) 551 self.assertJSONEqual( 552 response.content, 553 {"name": ["This field may not be blank."]}, 554 ) 555 556 # Test with tab and newline characters 557 response = self.client.post( 558 reverse("authentik_api:user-service-account"), 559 data={ 560 "name": "\t\n", 561 "create_group": True, 562 }, 563 ) 564 self.assertEqual(response.status_code, 400) 565 self.assertJSONEqual( 566 response.content, 567 {"name": ["This field may not be blank."]}, 568 ) 569 570 def test_service_account_validation_valid_username(self): 571 """Test service account creation with valid username""" 572 self.client.force_login(self.admin) 573 574 # Test with valid username 575 response = self.client.post( 576 reverse("authentik_api:user-service-account"), 577 data={ 578 "name": "valid-service-account", 579 "create_group": True, 580 }, 581 ) 582 self.assertEqual(response.status_code, 200) 583 584 # Verify response structure 585 body = loads(response.content) 586 self.assertIn("username", body) 587 self.assertIn("user_uid", body) 588 self.assertIn("user_pk", body) 589 self.assertIn("group_pk", body) # Should exist since create_group=True 590 self.assertIn("token", body) 591 592 # Verify field types 593 self.assertEqual(body["username"], "valid-service-account") 594 self.assertIsInstance(body["user_pk"], int) 595 self.assertIsInstance(body["user_uid"], str) 596 self.assertIsInstance(body["token"], str) 597 self.assertIsInstance(body["group_pk"], str) 598 599 def test_service_account_validation_without_group(self): 600 """Test service account creation without creating a group""" 601 self.client.force_login(self.admin) 602 603 response = self.client.post( 604 reverse("authentik_api:user-service-account"), 605 data={ 606 "name": "no-group-service-account", 607 "create_group": False, 608 }, 609 ) 610 self.assertEqual(response.status_code, 200) 611 612 body = loads(response.content) 613 self.assertIn("username", body) 614 self.assertIn("user_uid", body) 615 self.assertIn("user_pk", body) 616 self.assertIn("token", body) 617 # Should NOT have group_pk when create_group=False 618 self.assertNotIn("group_pk", body) 619 620 def test_service_account_validation_duplicate_username(self): 621 """Test service account creation with duplicate username""" 622 self.client.force_login(self.admin) 623 624 # Create first service account 625 response = self.client.post( 626 reverse("authentik_api:user-service-account"), 627 data={ 628 "name": "duplicate-test", 629 "create_group": True, 630 }, 631 ) 632 self.assertEqual(response.status_code, 200) 633 634 # Attempt to create second with same username 635 response = self.client.post( 636 reverse("authentik_api:user-service-account"), 637 data={ 638 "name": "duplicate-test", 639 "create_group": True, 640 }, 641 ) 642 self.assertEqual(response.status_code, 400) 643 self.assertJSONEqual( 644 response.content, 645 {"name": ["This field must be unique."]}, 646 ) 647 648 def test_service_account_validation_invalid_create_group(self): 649 """Test service account creation with invalid create_group field""" 650 self.client.force_login(self.admin) 651 652 # Test with string instead of boolean 653 response = self.client.post( 654 reverse("authentik_api:user-service-account"), 655 data={ 656 "name": "test-sa", 657 "create_group": "invalid", 658 }, 659 ) 660 self.assertEqual(response.status_code, 400) 661 self.assertJSONEqual( 662 response.content, 663 {"create_group": ["Must be a valid boolean."]}, 664 ) 665 666 # Test with number instead of boolean 667 response = self.client.post( 668 reverse("authentik_api:user-service-account"), 669 data={ 670 "name": "test-sa", 671 "create_group": 123, 672 }, 673 ) 674 self.assertEqual(response.status_code, 400) 675 self.assertJSONEqual( 676 response.content, 677 {"create_group": ["Must be a valid boolean."]}, 678 ) 679 680 def test_service_account_validation_invalid_expiring(self): 681 """Test service account creation with invalid expiring field""" 682 self.client.force_login(self.admin) 683 684 # Test with string instead of boolean 685 response = self.client.post( 686 reverse("authentik_api:user-service-account"), 687 data={ 688 "name": "test-sa", 689 "expiring": "invalid", 690 }, 691 ) 692 self.assertEqual(response.status_code, 400) 693 self.assertJSONEqual( 694 response.content, 695 {"expiring": ["Must be a valid boolean."]}, 696 ) 697 698 def test_service_account_validation_invalid_expires(self): 699 """Test service account creation with invalid expires field""" 700 self.client.force_login(self.admin) 701 702 # Test with invalid datetime string 703 response = self.client.post( 704 reverse("authentik_api:user-service-account"), 705 data={ 706 "name": "test-sa", 707 "expires": "invalid-datetime", 708 }, 709 ) 710 self.assertEqual(response.status_code, 400) 711 self.assertJSONEqual( 712 response.content, 713 { 714 "expires": [ 715 "Datetime has wrong format. Use one of these formats instead: " 716 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 717 ] 718 }, 719 ) 720 721 # Test with invalid format 722 response = self.client.post( 723 reverse("authentik_api:user-service-account"), 724 data={ 725 "name": "test-sa", 726 "expires": "2024-13-45", # Invalid month/day 727 }, 728 ) 729 self.assertEqual(response.status_code, 400) 730 self.assertJSONEqual( 731 response.content, 732 { 733 "expires": [ 734 "Datetime has wrong format. Use one of these formats instead: " 735 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 736 ] 737 }, 738 ) 739 740 def test_service_account_validation_multiple_errors(self): 741 """Test service account creation with multiple validation errors""" 742 self.client.force_login(self.admin) 743 744 response = self.client.post( 745 reverse("authentik_api:user-service-account"), 746 data={ 747 "name": "", # Empty username 748 "create_group": "invalid", # Invalid boolean 749 "expiring": 123, # Invalid boolean 750 "expires": "not-a-date", # Invalid datetime 751 }, 752 ) 753 self.assertEqual(response.status_code, 400) 754 self.assertJSONEqual( 755 response.content, 756 { 757 "name": ["This field may not be blank."], 758 "create_group": ["Must be a valid boolean."], 759 "expiring": ["Must be a valid boolean."], 760 "expires": [ 761 "Datetime has wrong format. Use one of these formats instead: " 762 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 763 ], 764 }, 765 ) 766 767 def test_service_account_validation_user_friendly_duplicate_error(self): 768 """Test that duplicate username returns user-friendly error, not database error""" 769 self.client.force_login(self.admin) 770 771 # Create first service account 772 response = self.client.post( 773 reverse("authentik_api:user-service-account"), 774 data={ 775 "name": "duplicate-username-test", 776 "create_group": True, 777 }, 778 ) 779 self.assertEqual(response.status_code, 200) 780 781 # Attempt to create second with same username 782 response = self.client.post( 783 reverse("authentik_api:user-service-account"), 784 data={ 785 "name": "duplicate-username-test", 786 "create_group": True, 787 }, 788 ) 789 self.assertEqual(response.status_code, 400) 790 self.assertJSONEqual( 791 response.content, 792 {"name": ["This field must be unique."]}, 793 ) 794 795 def test_filter_last_login(self): 796 """Test API filtering by last_login""" 797 from datetime import timedelta 798 799 from django.utils import timezone 800 801 User.objects.all().delete() 802 admin = create_test_admin_user() 803 self.client.force_login(admin) 804 805 # Create users with different last_login values 806 user_recent = create_test_user() 807 user_recent.last_login = timezone.now() 808 user_recent.save() 809 810 user_old = create_test_user() 811 user_old.last_login = timezone.now() - timedelta(days=400) # Over 1 year ago 812 user_old.save() 813 814 user_never = create_test_user() 815 user_never.last_login = None # Never logged in 816 user_never.save() 817 818 # Filter users who logged in before 1 year ago 819 one_year_ago = (timezone.now() - timedelta(days=365)).isoformat() 820 response = self.client.get( 821 reverse("authentik_api:user-list"), 822 data={"last_login__lt": one_year_ago}, 823 ) 824 self.assertEqual(response.status_code, 200) 825 body = loads(response.content) 826 self.assertEqual(len(body["results"]), 1) 827 self.assertEqual(body["results"][0]["pk"], user_old.pk) 828 829 # Filter users who have never logged in 830 response = self.client.get( 831 reverse("authentik_api:user-list"), 832 data={"last_login__isnull": True}, 833 ) 834 self.assertEqual(response.status_code, 200) 835 body = loads(response.content) 836 # Should include user_never and admin (who hasn't logged in via the app) 837 pks = [r["pk"] for r in body["results"]] 838 self.assertIn(user_never.pk, pks) 839 840 def test_sort_by_last_login(self): 841 """Test API sorting by last_login""" 842 from datetime import timedelta 843 844 from django.utils import timezone 845 846 User.objects.all().delete() 847 admin = create_test_admin_user() 848 self.client.force_login(admin) 849 850 user1 = create_test_user() 851 user1.last_login = timezone.now() - timedelta(days=10) 852 user1.save() 853 854 user2 = create_test_user() 855 user2.last_login = timezone.now() - timedelta(days=5) 856 user2.save() 857 858 # Ascending order (oldest first) 859 response = self.client.get( 860 reverse("authentik_api:user-list"), 861 data={"ordering": "last_login"}, 862 ) 863 self.assertEqual(response.status_code, 200) 864 body = loads(response.content) 865 # Users with null last_login come first, then user1 (older), then user2 (newer) 866 self.assertEqual(len(body["results"]), 3) 867 868 # Descending order (newest first) 869 response = self.client.get( 870 reverse("authentik_api:user-list"), 871 data={"ordering": "-last_login"}, 872 ) 873 self.assertEqual(response.status_code, 200) 874 body = loads(response.content) 875 # user2 should come before user1 (more recent login) 876 pks = [r["pk"] for r in body["results"]] 877 self.assertIn(user1.pk, pks) 878 self.assertIn(user2.pk, pks) 879 # Verify user2 comes before user1 in descending order 880 self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
31class TestUsersAPI(APITestCase): 32 """Test Users API""" 33 34 def setUp(self) -> None: 35 self.admin = create_test_admin_user() 36 self.user = create_test_user() 37 38 def test_filter_type(self): 39 """Test API filtering by type""" 40 self.client.force_login(self.admin) 41 user = create_test_admin_user(type=UserTypes.EXTERNAL) 42 response = self.client.get( 43 reverse("authentik_api:user-list"), 44 data={ 45 "type": UserTypes.EXTERNAL, 46 "username": user.username, 47 }, 48 ) 49 self.assertEqual(response.status_code, 200) 50 51 def test_filter_is_superuser(self): 52 """Test API filtering by superuser status""" 53 User.objects.all().delete() 54 admin = create_test_admin_user() 55 self.client.force_login(admin) 56 # Test superuser 57 response = self.client.get( 58 reverse("authentik_api:user-list"), 59 data={ 60 "is_superuser": True, 61 }, 62 ) 63 self.assertEqual(response.status_code, 200) 64 body = loads(response.content) 65 self.assertEqual(len(body["results"]), 1) 66 self.assertEqual(body["results"][0]["username"], admin.username) 67 # Test non-superuser 68 user = create_test_user() 69 response = self.client.get( 70 reverse("authentik_api:user-list"), 71 data={ 72 "is_superuser": False, 73 }, 74 ) 75 self.assertEqual(response.status_code, 200) 76 body = loads(response.content) 77 self.assertEqual(len(body["results"]), 1, body) 78 self.assertEqual(body["results"][0]["username"], user.username) 79 80 def test_list_with_groups(self): 81 """Test listing with groups""" 82 self.client.force_login(self.admin) 83 response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"}) 84 self.assertEqual(response.status_code, 200) 85 86 def test_recovery_no_flow(self): 87 """Test user recovery link (no recovery flow set)""" 88 self.client.force_login(self.admin) 89 response = self.client.post( 90 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 91 ) 92 self.assertEqual(response.status_code, 400) 93 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."}) 94 95 def test_set_password(self): 96 """Test Direct password set""" 97 self.client.force_login(self.admin) 98 new_pw = generate_key() 99 response = self.client.post( 100 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 101 data={"password": new_pw}, 102 ) 103 self.assertEqual(response.status_code, 204) 104 self.admin.refresh_from_db() 105 self.assertTrue(self.admin.check_password(new_pw)) 106 107 def test_set_password_blank(self): 108 """Test Direct password set""" 109 self.client.force_login(self.admin) 110 response = self.client.post( 111 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 112 data={"password": ""}, 113 ) 114 self.assertEqual(response.status_code, 400) 115 self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]}) 116 117 def test_recovery(self): 118 """Test user recovery link""" 119 flow = create_test_flow( 120 FlowDesignation.RECOVERY, 121 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 122 ) 123 brand: Brand = create_test_brand() 124 brand.flow_recovery = flow 125 brand.save() 126 self.client.force_login(self.admin) 127 response = self.client.post( 128 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 129 ) 130 self.assertEqual(response.status_code, 200) 131 132 def test_recovery_duration(self): 133 """Test user recovery token duration""" 134 Token.objects.all().delete() 135 flow = create_test_flow( 136 FlowDesignation.RECOVERY, 137 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 138 ) 139 brand: Brand = create_test_brand() 140 brand.flow_recovery = flow 141 brand.save() 142 self.client.force_login(self.admin) 143 response = self.client.post( 144 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 145 data={"token_duration": "days=33"}, 146 ) 147 self.assertEqual(response.status_code, 200) 148 expires = Token.objects.first().expires 149 expected_expires = now() + timedelta(days=33) 150 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 151 152 def test_recovery_duration_update(self): 153 """Test user recovery token duration update""" 154 Token.objects.all().delete() 155 flow = create_test_flow( 156 FlowDesignation.RECOVERY, 157 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 158 ) 159 brand: Brand = create_test_brand() 160 brand.flow_recovery = flow 161 brand.save() 162 self.client.force_login(self.admin) 163 response = self.client.post( 164 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 165 data={"token_duration": "days=33"}, 166 ) 167 self.assertEqual(response.status_code, 200) 168 expires = Token.objects.first().expires 169 expected_expires = now() + timedelta(days=33) 170 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 171 response = self.client.post( 172 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 173 data={"token_duration": "days=66"}, 174 ) 175 expires = Token.objects.first().expires 176 expected_expires = now() + timedelta(days=66) 177 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 178 179 def test_recovery_email_no_flow(self): 180 """Test user recovery link (no recovery flow set)""" 181 self.client.force_login(self.admin) 182 self.user.email = "" 183 self.user.save() 184 stage = EmailStage.objects.create(name="email") 185 response = self.client.post( 186 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 187 data={"email_stage": stage.pk}, 188 ) 189 self.assertEqual(response.status_code, 400) 190 self.assertJSONEqual( 191 response.content, {"non_field_errors": "User does not have an email address set."} 192 ) 193 self.user.email = "foo@bar.baz" 194 self.user.save() 195 response = self.client.post( 196 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 197 data={"email_stage": stage.pk}, 198 ) 199 self.assertEqual(response.status_code, 400) 200 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."}) 201 202 def test_recovery_email_no_stage(self): 203 """Test user recovery link (no email stage)""" 204 self.user.email = "foo@bar.baz" 205 self.user.save() 206 flow = create_test_flow(designation=FlowDesignation.RECOVERY) 207 brand: Brand = create_test_brand() 208 brand.flow_recovery = flow 209 brand.save() 210 self.client.force_login(self.admin) 211 response = self.client.post( 212 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) 213 ) 214 self.assertEqual(response.status_code, 400) 215 self.assertJSONEqual(response.content, {"email_stage": ["This field is required."]}) 216 217 def test_recovery_email(self): 218 """Test user recovery link""" 219 self.user.email = "foo@bar.baz" 220 self.user.save() 221 flow = create_test_flow(FlowDesignation.RECOVERY) 222 brand: Brand = create_test_brand() 223 brand.flow_recovery = flow 224 brand.save() 225 226 stage = EmailStage.objects.create(name="email") 227 228 self.client.force_login(self.admin) 229 response = self.client.post( 230 reverse( 231 "authentik_api:user-recovery-email", 232 kwargs={"pk": self.user.pk}, 233 ), 234 data={"email_stage": stage.pk}, 235 ) 236 self.assertEqual(response.status_code, 204) 237 238 def test_service_account(self): 239 """Service account creation""" 240 self.client.force_login(self.admin) 241 response = self.client.post(reverse("authentik_api:user-service-account")) 242 self.assertEqual(response.status_code, 400) 243 response = self.client.post( 244 reverse("authentik_api:user-service-account"), 245 data={ 246 "name": "test-sa", 247 "create_group": True, 248 }, 249 ) 250 self.assertEqual(response.status_code, 200) 251 252 user_filter = User.objects.filter( 253 username="test-sa", 254 type=UserTypes.SERVICE_ACCOUNT, 255 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 256 ) 257 self.assertTrue(user_filter.exists()) 258 user: User = user_filter.first() 259 self.assertFalse(user.has_usable_password()) 260 261 token_filter = Token.objects.filter(user=user) 262 self.assertTrue(token_filter.exists()) 263 self.assertTrue(token_filter.first().expiring) 264 265 def test_service_account_no_expire(self): 266 """Service account creation without token expiration""" 267 self.client.force_login(self.admin) 268 response = self.client.post( 269 reverse("authentik_api:user-service-account"), 270 data={ 271 "name": "test-sa", 272 "create_group": True, 273 "expiring": False, 274 }, 275 ) 276 self.assertEqual(response.status_code, 200) 277 278 user_filter = User.objects.filter( 279 username="test-sa", 280 type=UserTypes.SERVICE_ACCOUNT, 281 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False}, 282 ) 283 self.assertTrue(user_filter.exists()) 284 user: User = user_filter.first() 285 self.assertFalse(user.has_usable_password()) 286 287 token_filter = Token.objects.filter(user=user) 288 self.assertTrue(token_filter.exists()) 289 self.assertFalse(token_filter.first().expiring) 290 291 def test_service_account_with_custom_expire(self): 292 """Service account creation with custom token expiration date""" 293 self.client.force_login(self.admin) 294 expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone() 295 response = self.client.post( 296 reverse("authentik_api:user-service-account"), 297 data={ 298 "name": "test-sa", 299 "create_group": True, 300 "expires": expire_on.isoformat(), 301 }, 302 ) 303 self.assertEqual(response.status_code, 200) 304 305 user_filter = User.objects.filter( 306 username="test-sa", 307 type=UserTypes.SERVICE_ACCOUNT, 308 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 309 ) 310 self.assertTrue(user_filter.exists()) 311 user: User = user_filter.first() 312 self.assertFalse(user.has_usable_password()) 313 314 token_filter = Token.objects.filter(user=user) 315 self.assertTrue(token_filter.exists()) 316 token = token_filter.first() 317 self.assertTrue(token.expiring) 318 self.assertEqual(token.expires, expire_on) 319 320 def test_service_account_invalid(self): 321 """Service account creation (twice with same name, expect error)""" 322 self.client.force_login(self.admin) 323 response = self.client.post( 324 reverse("authentik_api:user-service-account"), 325 data={ 326 "name": "test-sa", 327 "create_group": True, 328 }, 329 ) 330 self.assertEqual(response.status_code, 200) 331 332 user_filter = User.objects.filter( 333 username="test-sa", 334 type=UserTypes.SERVICE_ACCOUNT, 335 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 336 ) 337 self.assertTrue(user_filter.exists()) 338 user: User = user_filter.first() 339 self.assertFalse(user.has_usable_password()) 340 341 token_filter = Token.objects.filter(user=user) 342 self.assertTrue(token_filter.exists()) 343 self.assertTrue(token_filter.first().expiring) 344 345 response = self.client.post( 346 reverse("authentik_api:user-service-account"), 347 data={ 348 "name": "test-sa", 349 "create_group": True, 350 }, 351 ) 352 self.assertEqual(response.status_code, 400) 353 354 def test_paths(self): 355 """Test path""" 356 self.client.force_login(self.admin) 357 response = self.client.get( 358 reverse("authentik_api:user-paths"), 359 ) 360 self.assertEqual(response.status_code, 200) 361 expected = list( 362 User.objects.all() 363 .values("path") 364 .distinct() 365 .order_by("path") 366 .values_list("path", flat=True) 367 ) 368 self.assertJSONEqual(response.content.decode(), {"paths": expected}) 369 370 def test_path_valid(self): 371 """Test path""" 372 self.client.force_login(self.admin) 373 response = self.client.post( 374 reverse("authentik_api:user-list"), 375 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo"}, 376 ) 377 self.assertEqual(response.status_code, 201) 378 379 def test_path_invalid(self): 380 """Test path (invalid)""" 381 self.client.force_login(self.admin) 382 response = self.client.post( 383 reverse("authentik_api:user-list"), 384 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "/foo"}, 385 ) 386 self.assertEqual(response.status_code, 400) 387 self.assertJSONEqual( 388 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 389 ) 390 391 self.client.force_login(self.admin) 392 response = self.client.post( 393 reverse("authentik_api:user-list"), 394 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": ""}, 395 ) 396 self.assertEqual(response.status_code, 400) 397 self.assertJSONEqual(response.content.decode(), {"path": ["This field may not be blank."]}) 398 399 response = self.client.post( 400 reverse("authentik_api:user-list"), 401 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo/"}, 402 ) 403 self.assertEqual(response.status_code, 400) 404 self.assertJSONEqual( 405 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 406 ) 407 408 response = self.client.post( 409 reverse("authentik_api:user-list"), 410 data={ 411 "name": generate_id(), 412 "username": generate_id(), 413 "groups": [], 414 "path": "fos//o", 415 }, 416 ) 417 self.assertEqual(response.status_code, 400) 418 self.assertJSONEqual( 419 response.content.decode(), {"path": ["No empty segments in user path allowed."]} 420 ) 421 422 def test_me(self): 423 """Test user's me endpoint""" 424 self.client.force_login(self.admin) 425 response = self.client.get(reverse("authentik_api:user-me")) 426 self.assertEqual(response.status_code, 200) 427 428 def test_session_delete(self): 429 """Ensure sessions are deleted when a user is deactivated""" 430 user = create_test_admin_user() 431 session_id = generate_id() 432 session = Session.objects.create( 433 session_key=session_id, 434 last_ip="255.255.255.255", 435 last_user_agent="", 436 ) 437 AuthenticatedSession.objects.create( 438 session=session, 439 user=user, 440 ) 441 442 self.client.force_login(self.admin) 443 response = self.client.patch( 444 reverse("authentik_api:user-detail", kwargs={"pk": user.pk}), 445 data={ 446 "is_active": False, 447 }, 448 ) 449 self.assertEqual(response.status_code, 200) 450 451 self.assertFalse(Session.objects.filter(session_key=session_id).exists()) 452 self.assertFalse( 453 AuthenticatedSession.objects.filter(session__session_key=session_id).exists() 454 ) 455 456 def test_sort_by_last_updated(self): 457 """Test API sorting by last_updated""" 458 User.objects.all().delete() 459 admin = create_test_admin_user() 460 self.client.force_login(admin) 461 462 user = create_test_user() 463 admin.first_name = "Sample change" 464 admin.last_name = "To trigger an update" 465 admin.save() 466 467 # Ascending 468 response = self.client.get( 469 reverse("authentik_api:user-list"), 470 data={ 471 "ordering": "last_updated", 472 }, 473 ) 474 self.assertEqual(response.status_code, 200) 475 476 body = loads(response.content) 477 self.assertEqual(len(body["results"]), 2) 478 self.assertEqual(body["results"][0]["pk"], user.pk) 479 480 # Descending 481 response = self.client.get( 482 reverse("authentik_api:user-list"), 483 data={ 484 "ordering": "-last_updated", 485 }, 486 ) 487 self.assertEqual(response.status_code, 200) 488 489 body = loads(response.content) 490 self.assertEqual(len(body["results"]), 2) 491 self.assertEqual(body["results"][0]["pk"], admin.pk) 492 493 def test_sort_by_date_joined(self): 494 """Test API sorting by date_joined""" 495 User.objects.all().delete() 496 admin = create_test_admin_user() 497 self.client.force_login(admin) 498 499 user = create_test_user() 500 501 response = self.client.get( 502 reverse("authentik_api:user-list"), 503 data={ 504 "ordering": "date_joined", 505 }, 506 ) 507 self.assertEqual(response.status_code, 200) 508 509 body = loads(response.content) 510 self.assertEqual(len(body["results"]), 2) 511 self.assertEqual(body["results"][0]["pk"], admin.pk) 512 513 response = self.client.get( 514 reverse("authentik_api:user-list"), 515 data={ 516 "ordering": "-date_joined", 517 }, 518 ) 519 self.assertEqual(response.status_code, 200) 520 521 body = loads(response.content) 522 self.assertEqual(len(body["results"]), 2) 523 self.assertEqual(body["results"][0]["pk"], user.pk) 524 525 def test_service_account_validation_empty_username(self): 526 """Test service account creation with empty/blank username validation""" 527 self.client.force_login(self.admin) 528 529 # Test with empty string 530 response = self.client.post( 531 reverse("authentik_api:user-service-account"), 532 data={ 533 "name": "", 534 "create_group": True, 535 }, 536 ) 537 self.assertEqual(response.status_code, 400) 538 self.assertJSONEqual( 539 response.content, 540 {"name": ["This field may not be blank."]}, 541 ) 542 543 # Test with only whitespace 544 response = self.client.post( 545 reverse("authentik_api:user-service-account"), 546 data={ 547 "name": " ", 548 "create_group": True, 549 }, 550 ) 551 self.assertEqual(response.status_code, 400) 552 self.assertJSONEqual( 553 response.content, 554 {"name": ["This field may not be blank."]}, 555 ) 556 557 # Test with tab and newline characters 558 response = self.client.post( 559 reverse("authentik_api:user-service-account"), 560 data={ 561 "name": "\t\n", 562 "create_group": True, 563 }, 564 ) 565 self.assertEqual(response.status_code, 400) 566 self.assertJSONEqual( 567 response.content, 568 {"name": ["This field may not be blank."]}, 569 ) 570 571 def test_service_account_validation_valid_username(self): 572 """Test service account creation with valid username""" 573 self.client.force_login(self.admin) 574 575 # Test with valid username 576 response = self.client.post( 577 reverse("authentik_api:user-service-account"), 578 data={ 579 "name": "valid-service-account", 580 "create_group": True, 581 }, 582 ) 583 self.assertEqual(response.status_code, 200) 584 585 # Verify response structure 586 body = loads(response.content) 587 self.assertIn("username", body) 588 self.assertIn("user_uid", body) 589 self.assertIn("user_pk", body) 590 self.assertIn("group_pk", body) # Should exist since create_group=True 591 self.assertIn("token", body) 592 593 # Verify field types 594 self.assertEqual(body["username"], "valid-service-account") 595 self.assertIsInstance(body["user_pk"], int) 596 self.assertIsInstance(body["user_uid"], str) 597 self.assertIsInstance(body["token"], str) 598 self.assertIsInstance(body["group_pk"], str) 599 600 def test_service_account_validation_without_group(self): 601 """Test service account creation without creating a group""" 602 self.client.force_login(self.admin) 603 604 response = self.client.post( 605 reverse("authentik_api:user-service-account"), 606 data={ 607 "name": "no-group-service-account", 608 "create_group": False, 609 }, 610 ) 611 self.assertEqual(response.status_code, 200) 612 613 body = loads(response.content) 614 self.assertIn("username", body) 615 self.assertIn("user_uid", body) 616 self.assertIn("user_pk", body) 617 self.assertIn("token", body) 618 # Should NOT have group_pk when create_group=False 619 self.assertNotIn("group_pk", body) 620 621 def test_service_account_validation_duplicate_username(self): 622 """Test service account creation with duplicate username""" 623 self.client.force_login(self.admin) 624 625 # Create first service account 626 response = self.client.post( 627 reverse("authentik_api:user-service-account"), 628 data={ 629 "name": "duplicate-test", 630 "create_group": True, 631 }, 632 ) 633 self.assertEqual(response.status_code, 200) 634 635 # Attempt to create second with same username 636 response = self.client.post( 637 reverse("authentik_api:user-service-account"), 638 data={ 639 "name": "duplicate-test", 640 "create_group": True, 641 }, 642 ) 643 self.assertEqual(response.status_code, 400) 644 self.assertJSONEqual( 645 response.content, 646 {"name": ["This field must be unique."]}, 647 ) 648 649 def test_service_account_validation_invalid_create_group(self): 650 """Test service account creation with invalid create_group field""" 651 self.client.force_login(self.admin) 652 653 # Test with string instead of boolean 654 response = self.client.post( 655 reverse("authentik_api:user-service-account"), 656 data={ 657 "name": "test-sa", 658 "create_group": "invalid", 659 }, 660 ) 661 self.assertEqual(response.status_code, 400) 662 self.assertJSONEqual( 663 response.content, 664 {"create_group": ["Must be a valid boolean."]}, 665 ) 666 667 # Test with number instead of boolean 668 response = self.client.post( 669 reverse("authentik_api:user-service-account"), 670 data={ 671 "name": "test-sa", 672 "create_group": 123, 673 }, 674 ) 675 self.assertEqual(response.status_code, 400) 676 self.assertJSONEqual( 677 response.content, 678 {"create_group": ["Must be a valid boolean."]}, 679 ) 680 681 def test_service_account_validation_invalid_expiring(self): 682 """Test service account creation with invalid expiring field""" 683 self.client.force_login(self.admin) 684 685 # Test with string instead of boolean 686 response = self.client.post( 687 reverse("authentik_api:user-service-account"), 688 data={ 689 "name": "test-sa", 690 "expiring": "invalid", 691 }, 692 ) 693 self.assertEqual(response.status_code, 400) 694 self.assertJSONEqual( 695 response.content, 696 {"expiring": ["Must be a valid boolean."]}, 697 ) 698 699 def test_service_account_validation_invalid_expires(self): 700 """Test service account creation with invalid expires field""" 701 self.client.force_login(self.admin) 702 703 # Test with invalid datetime string 704 response = self.client.post( 705 reverse("authentik_api:user-service-account"), 706 data={ 707 "name": "test-sa", 708 "expires": "invalid-datetime", 709 }, 710 ) 711 self.assertEqual(response.status_code, 400) 712 self.assertJSONEqual( 713 response.content, 714 { 715 "expires": [ 716 "Datetime has wrong format. Use one of these formats instead: " 717 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 718 ] 719 }, 720 ) 721 722 # Test with invalid format 723 response = self.client.post( 724 reverse("authentik_api:user-service-account"), 725 data={ 726 "name": "test-sa", 727 "expires": "2024-13-45", # Invalid month/day 728 }, 729 ) 730 self.assertEqual(response.status_code, 400) 731 self.assertJSONEqual( 732 response.content, 733 { 734 "expires": [ 735 "Datetime has wrong format. Use one of these formats instead: " 736 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 737 ] 738 }, 739 ) 740 741 def test_service_account_validation_multiple_errors(self): 742 """Test service account creation with multiple validation errors""" 743 self.client.force_login(self.admin) 744 745 response = self.client.post( 746 reverse("authentik_api:user-service-account"), 747 data={ 748 "name": "", # Empty username 749 "create_group": "invalid", # Invalid boolean 750 "expiring": 123, # Invalid boolean 751 "expires": "not-a-date", # Invalid datetime 752 }, 753 ) 754 self.assertEqual(response.status_code, 400) 755 self.assertJSONEqual( 756 response.content, 757 { 758 "name": ["This field may not be blank."], 759 "create_group": ["Must be a valid boolean."], 760 "expiring": ["Must be a valid boolean."], 761 "expires": [ 762 "Datetime has wrong format. Use one of these formats instead: " 763 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 764 ], 765 }, 766 ) 767 768 def test_service_account_validation_user_friendly_duplicate_error(self): 769 """Test that duplicate username returns user-friendly error, not database error""" 770 self.client.force_login(self.admin) 771 772 # Create first service account 773 response = self.client.post( 774 reverse("authentik_api:user-service-account"), 775 data={ 776 "name": "duplicate-username-test", 777 "create_group": True, 778 }, 779 ) 780 self.assertEqual(response.status_code, 200) 781 782 # Attempt to create second with same username 783 response = self.client.post( 784 reverse("authentik_api:user-service-account"), 785 data={ 786 "name": "duplicate-username-test", 787 "create_group": True, 788 }, 789 ) 790 self.assertEqual(response.status_code, 400) 791 self.assertJSONEqual( 792 response.content, 793 {"name": ["This field must be unique."]}, 794 ) 795 796 def test_filter_last_login(self): 797 """Test API filtering by last_login""" 798 from datetime import timedelta 799 800 from django.utils import timezone 801 802 User.objects.all().delete() 803 admin = create_test_admin_user() 804 self.client.force_login(admin) 805 806 # Create users with different last_login values 807 user_recent = create_test_user() 808 user_recent.last_login = timezone.now() 809 user_recent.save() 810 811 user_old = create_test_user() 812 user_old.last_login = timezone.now() - timedelta(days=400) # Over 1 year ago 813 user_old.save() 814 815 user_never = create_test_user() 816 user_never.last_login = None # Never logged in 817 user_never.save() 818 819 # Filter users who logged in before 1 year ago 820 one_year_ago = (timezone.now() - timedelta(days=365)).isoformat() 821 response = self.client.get( 822 reverse("authentik_api:user-list"), 823 data={"last_login__lt": one_year_ago}, 824 ) 825 self.assertEqual(response.status_code, 200) 826 body = loads(response.content) 827 self.assertEqual(len(body["results"]), 1) 828 self.assertEqual(body["results"][0]["pk"], user_old.pk) 829 830 # Filter users who have never logged in 831 response = self.client.get( 832 reverse("authentik_api:user-list"), 833 data={"last_login__isnull": True}, 834 ) 835 self.assertEqual(response.status_code, 200) 836 body = loads(response.content) 837 # Should include user_never and admin (who hasn't logged in via the app) 838 pks = [r["pk"] for r in body["results"]] 839 self.assertIn(user_never.pk, pks) 840 841 def test_sort_by_last_login(self): 842 """Test API sorting by last_login""" 843 from datetime import timedelta 844 845 from django.utils import timezone 846 847 User.objects.all().delete() 848 admin = create_test_admin_user() 849 self.client.force_login(admin) 850 851 user1 = create_test_user() 852 user1.last_login = timezone.now() - timedelta(days=10) 853 user1.save() 854 855 user2 = create_test_user() 856 user2.last_login = timezone.now() - timedelta(days=5) 857 user2.save() 858 859 # Ascending order (oldest first) 860 response = self.client.get( 861 reverse("authentik_api:user-list"), 862 data={"ordering": "last_login"}, 863 ) 864 self.assertEqual(response.status_code, 200) 865 body = loads(response.content) 866 # Users with null last_login come first, then user1 (older), then user2 (newer) 867 self.assertEqual(len(body["results"]), 3) 868 869 # Descending order (newest first) 870 response = self.client.get( 871 reverse("authentik_api:user-list"), 872 data={"ordering": "-last_login"}, 873 ) 874 self.assertEqual(response.status_code, 200) 875 body = loads(response.content) 876 # user2 should come before user1 (more recent login) 877 pks = [r["pk"] for r in body["results"]] 878 self.assertIn(user1.pk, pks) 879 self.assertIn(user2.pk, pks) 880 # Verify user2 comes before user1 in descending order 881 self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
Test Users API
34 def setUp(self) -> None: 35 self.admin = create_test_admin_user() 36 self.user = create_test_user()
Hook method for setting up the test fixture before exercising it.
38 def test_filter_type(self): 39 """Test API filtering by type""" 40 self.client.force_login(self.admin) 41 user = create_test_admin_user(type=UserTypes.EXTERNAL) 42 response = self.client.get( 43 reverse("authentik_api:user-list"), 44 data={ 45 "type": UserTypes.EXTERNAL, 46 "username": user.username, 47 }, 48 ) 49 self.assertEqual(response.status_code, 200)
Test API filtering by type
51 def test_filter_is_superuser(self): 52 """Test API filtering by superuser status""" 53 User.objects.all().delete() 54 admin = create_test_admin_user() 55 self.client.force_login(admin) 56 # Test superuser 57 response = self.client.get( 58 reverse("authentik_api:user-list"), 59 data={ 60 "is_superuser": True, 61 }, 62 ) 63 self.assertEqual(response.status_code, 200) 64 body = loads(response.content) 65 self.assertEqual(len(body["results"]), 1) 66 self.assertEqual(body["results"][0]["username"], admin.username) 67 # Test non-superuser 68 user = create_test_user() 69 response = self.client.get( 70 reverse("authentik_api:user-list"), 71 data={ 72 "is_superuser": False, 73 }, 74 ) 75 self.assertEqual(response.status_code, 200) 76 body = loads(response.content) 77 self.assertEqual(len(body["results"]), 1, body) 78 self.assertEqual(body["results"][0]["username"], user.username)
Test API filtering by superuser status
80 def test_list_with_groups(self): 81 """Test listing with groups""" 82 self.client.force_login(self.admin) 83 response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"}) 84 self.assertEqual(response.status_code, 200)
Test listing with groups
86 def test_recovery_no_flow(self): 87 """Test user recovery link (no recovery flow set)""" 88 self.client.force_login(self.admin) 89 response = self.client.post( 90 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 91 ) 92 self.assertEqual(response.status_code, 400) 93 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."})
Test user recovery link (no recovery flow set)
95 def test_set_password(self): 96 """Test Direct password set""" 97 self.client.force_login(self.admin) 98 new_pw = generate_key() 99 response = self.client.post( 100 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 101 data={"password": new_pw}, 102 ) 103 self.assertEqual(response.status_code, 204) 104 self.admin.refresh_from_db() 105 self.assertTrue(self.admin.check_password(new_pw))
Test Direct password set
107 def test_set_password_blank(self): 108 """Test Direct password set""" 109 self.client.force_login(self.admin) 110 response = self.client.post( 111 reverse("authentik_api:user-set-password", kwargs={"pk": self.admin.pk}), 112 data={"password": ""}, 113 ) 114 self.assertEqual(response.status_code, 400) 115 self.assertJSONEqual(response.content, {"password": ["This field may not be blank."]})
Test Direct password set
117 def test_recovery(self): 118 """Test user recovery link""" 119 flow = create_test_flow( 120 FlowDesignation.RECOVERY, 121 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 122 ) 123 brand: Brand = create_test_brand() 124 brand.flow_recovery = flow 125 brand.save() 126 self.client.force_login(self.admin) 127 response = self.client.post( 128 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}) 129 ) 130 self.assertEqual(response.status_code, 200)
Test user recovery link
132 def test_recovery_duration(self): 133 """Test user recovery token duration""" 134 Token.objects.all().delete() 135 flow = create_test_flow( 136 FlowDesignation.RECOVERY, 137 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 138 ) 139 brand: Brand = create_test_brand() 140 brand.flow_recovery = flow 141 brand.save() 142 self.client.force_login(self.admin) 143 response = self.client.post( 144 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 145 data={"token_duration": "days=33"}, 146 ) 147 self.assertEqual(response.status_code, 200) 148 expires = Token.objects.first().expires 149 expected_expires = now() + timedelta(days=33) 150 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1))
Test user recovery token duration
152 def test_recovery_duration_update(self): 153 """Test user recovery token duration update""" 154 Token.objects.all().delete() 155 flow = create_test_flow( 156 FlowDesignation.RECOVERY, 157 authentication=FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED, 158 ) 159 brand: Brand = create_test_brand() 160 brand.flow_recovery = flow 161 brand.save() 162 self.client.force_login(self.admin) 163 response = self.client.post( 164 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 165 data={"token_duration": "days=33"}, 166 ) 167 self.assertEqual(response.status_code, 200) 168 expires = Token.objects.first().expires 169 expected_expires = now() + timedelta(days=33) 170 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1)) 171 response = self.client.post( 172 reverse("authentik_api:user-recovery", kwargs={"pk": self.user.pk}), 173 data={"token_duration": "days=66"}, 174 ) 175 expires = Token.objects.first().expires 176 expected_expires = now() + timedelta(days=66) 177 self.assertTrue(timedelta(minutes=-1) < expected_expires - expires < timedelta(minutes=1))
Test user recovery token duration update
179 def test_recovery_email_no_flow(self): 180 """Test user recovery link (no recovery flow set)""" 181 self.client.force_login(self.admin) 182 self.user.email = "" 183 self.user.save() 184 stage = EmailStage.objects.create(name="email") 185 response = self.client.post( 186 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 187 data={"email_stage": stage.pk}, 188 ) 189 self.assertEqual(response.status_code, 400) 190 self.assertJSONEqual( 191 response.content, {"non_field_errors": "User does not have an email address set."} 192 ) 193 self.user.email = "foo@bar.baz" 194 self.user.save() 195 response = self.client.post( 196 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}), 197 data={"email_stage": stage.pk}, 198 ) 199 self.assertEqual(response.status_code, 400) 200 self.assertJSONEqual(response.content, {"non_field_errors": "No recovery flow set."})
Test user recovery link (no recovery flow set)
202 def test_recovery_email_no_stage(self): 203 """Test user recovery link (no email stage)""" 204 self.user.email = "foo@bar.baz" 205 self.user.save() 206 flow = create_test_flow(designation=FlowDesignation.RECOVERY) 207 brand: Brand = create_test_brand() 208 brand.flow_recovery = flow 209 brand.save() 210 self.client.force_login(self.admin) 211 response = self.client.post( 212 reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) 213 ) 214 self.assertEqual(response.status_code, 400) 215 self.assertJSONEqual(response.content, {"email_stage": ["This field is required."]})
Test user recovery link (no email stage)
217 def test_recovery_email(self): 218 """Test user recovery link""" 219 self.user.email = "foo@bar.baz" 220 self.user.save() 221 flow = create_test_flow(FlowDesignation.RECOVERY) 222 brand: Brand = create_test_brand() 223 brand.flow_recovery = flow 224 brand.save() 225 226 stage = EmailStage.objects.create(name="email") 227 228 self.client.force_login(self.admin) 229 response = self.client.post( 230 reverse( 231 "authentik_api:user-recovery-email", 232 kwargs={"pk": self.user.pk}, 233 ), 234 data={"email_stage": stage.pk}, 235 ) 236 self.assertEqual(response.status_code, 204)
Test user recovery link
238 def test_service_account(self): 239 """Service account creation""" 240 self.client.force_login(self.admin) 241 response = self.client.post(reverse("authentik_api:user-service-account")) 242 self.assertEqual(response.status_code, 400) 243 response = self.client.post( 244 reverse("authentik_api:user-service-account"), 245 data={ 246 "name": "test-sa", 247 "create_group": True, 248 }, 249 ) 250 self.assertEqual(response.status_code, 200) 251 252 user_filter = User.objects.filter( 253 username="test-sa", 254 type=UserTypes.SERVICE_ACCOUNT, 255 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 256 ) 257 self.assertTrue(user_filter.exists()) 258 user: User = user_filter.first() 259 self.assertFalse(user.has_usable_password()) 260 261 token_filter = Token.objects.filter(user=user) 262 self.assertTrue(token_filter.exists()) 263 self.assertTrue(token_filter.first().expiring)
Service account creation
265 def test_service_account_no_expire(self): 266 """Service account creation without token expiration""" 267 self.client.force_login(self.admin) 268 response = self.client.post( 269 reverse("authentik_api:user-service-account"), 270 data={ 271 "name": "test-sa", 272 "create_group": True, 273 "expiring": False, 274 }, 275 ) 276 self.assertEqual(response.status_code, 200) 277 278 user_filter = User.objects.filter( 279 username="test-sa", 280 type=UserTypes.SERVICE_ACCOUNT, 281 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False}, 282 ) 283 self.assertTrue(user_filter.exists()) 284 user: User = user_filter.first() 285 self.assertFalse(user.has_usable_password()) 286 287 token_filter = Token.objects.filter(user=user) 288 self.assertTrue(token_filter.exists()) 289 self.assertFalse(token_filter.first().expiring)
Service account creation without token expiration
291 def test_service_account_with_custom_expire(self): 292 """Service account creation with custom token expiration date""" 293 self.client.force_login(self.admin) 294 expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone() 295 response = self.client.post( 296 reverse("authentik_api:user-service-account"), 297 data={ 298 "name": "test-sa", 299 "create_group": True, 300 "expires": expire_on.isoformat(), 301 }, 302 ) 303 self.assertEqual(response.status_code, 200) 304 305 user_filter = User.objects.filter( 306 username="test-sa", 307 type=UserTypes.SERVICE_ACCOUNT, 308 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 309 ) 310 self.assertTrue(user_filter.exists()) 311 user: User = user_filter.first() 312 self.assertFalse(user.has_usable_password()) 313 314 token_filter = Token.objects.filter(user=user) 315 self.assertTrue(token_filter.exists()) 316 token = token_filter.first() 317 self.assertTrue(token.expiring) 318 self.assertEqual(token.expires, expire_on)
Service account creation with custom token expiration date
320 def test_service_account_invalid(self): 321 """Service account creation (twice with same name, expect error)""" 322 self.client.force_login(self.admin) 323 response = self.client.post( 324 reverse("authentik_api:user-service-account"), 325 data={ 326 "name": "test-sa", 327 "create_group": True, 328 }, 329 ) 330 self.assertEqual(response.status_code, 200) 331 332 user_filter = User.objects.filter( 333 username="test-sa", 334 type=UserTypes.SERVICE_ACCOUNT, 335 attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True}, 336 ) 337 self.assertTrue(user_filter.exists()) 338 user: User = user_filter.first() 339 self.assertFalse(user.has_usable_password()) 340 341 token_filter = Token.objects.filter(user=user) 342 self.assertTrue(token_filter.exists()) 343 self.assertTrue(token_filter.first().expiring) 344 345 response = self.client.post( 346 reverse("authentik_api:user-service-account"), 347 data={ 348 "name": "test-sa", 349 "create_group": True, 350 }, 351 ) 352 self.assertEqual(response.status_code, 400)
Service account creation (twice with same name, expect error)
354 def test_paths(self): 355 """Test path""" 356 self.client.force_login(self.admin) 357 response = self.client.get( 358 reverse("authentik_api:user-paths"), 359 ) 360 self.assertEqual(response.status_code, 200) 361 expected = list( 362 User.objects.all() 363 .values("path") 364 .distinct() 365 .order_by("path") 366 .values_list("path", flat=True) 367 ) 368 self.assertJSONEqual(response.content.decode(), {"paths": expected})
Test path
370 def test_path_valid(self): 371 """Test path""" 372 self.client.force_login(self.admin) 373 response = self.client.post( 374 reverse("authentik_api:user-list"), 375 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo"}, 376 ) 377 self.assertEqual(response.status_code, 201)
Test path
379 def test_path_invalid(self): 380 """Test path (invalid)""" 381 self.client.force_login(self.admin) 382 response = self.client.post( 383 reverse("authentik_api:user-list"), 384 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "/foo"}, 385 ) 386 self.assertEqual(response.status_code, 400) 387 self.assertJSONEqual( 388 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 389 ) 390 391 self.client.force_login(self.admin) 392 response = self.client.post( 393 reverse("authentik_api:user-list"), 394 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": ""}, 395 ) 396 self.assertEqual(response.status_code, 400) 397 self.assertJSONEqual(response.content.decode(), {"path": ["This field may not be blank."]}) 398 399 response = self.client.post( 400 reverse("authentik_api:user-list"), 401 data={"name": generate_id(), "username": generate_id(), "groups": [], "path": "foo/"}, 402 ) 403 self.assertEqual(response.status_code, 400) 404 self.assertJSONEqual( 405 response.content.decode(), {"path": ["No leading or trailing slashes allowed."]} 406 ) 407 408 response = self.client.post( 409 reverse("authentik_api:user-list"), 410 data={ 411 "name": generate_id(), 412 "username": generate_id(), 413 "groups": [], 414 "path": "fos//o", 415 }, 416 ) 417 self.assertEqual(response.status_code, 400) 418 self.assertJSONEqual( 419 response.content.decode(), {"path": ["No empty segments in user path allowed."]} 420 )
Test path (invalid)
422 def test_me(self): 423 """Test user's me endpoint""" 424 self.client.force_login(self.admin) 425 response = self.client.get(reverse("authentik_api:user-me")) 426 self.assertEqual(response.status_code, 200)
Test user's me endpoint
428 def test_session_delete(self): 429 """Ensure sessions are deleted when a user is deactivated""" 430 user = create_test_admin_user() 431 session_id = generate_id() 432 session = Session.objects.create( 433 session_key=session_id, 434 last_ip="255.255.255.255", 435 last_user_agent="", 436 ) 437 AuthenticatedSession.objects.create( 438 session=session, 439 user=user, 440 ) 441 442 self.client.force_login(self.admin) 443 response = self.client.patch( 444 reverse("authentik_api:user-detail", kwargs={"pk": user.pk}), 445 data={ 446 "is_active": False, 447 }, 448 ) 449 self.assertEqual(response.status_code, 200) 450 451 self.assertFalse(Session.objects.filter(session_key=session_id).exists()) 452 self.assertFalse( 453 AuthenticatedSession.objects.filter(session__session_key=session_id).exists() 454 )
Ensure sessions are deleted when a user is deactivated
456 def test_sort_by_last_updated(self): 457 """Test API sorting by last_updated""" 458 User.objects.all().delete() 459 admin = create_test_admin_user() 460 self.client.force_login(admin) 461 462 user = create_test_user() 463 admin.first_name = "Sample change" 464 admin.last_name = "To trigger an update" 465 admin.save() 466 467 # Ascending 468 response = self.client.get( 469 reverse("authentik_api:user-list"), 470 data={ 471 "ordering": "last_updated", 472 }, 473 ) 474 self.assertEqual(response.status_code, 200) 475 476 body = loads(response.content) 477 self.assertEqual(len(body["results"]), 2) 478 self.assertEqual(body["results"][0]["pk"], user.pk) 479 480 # Descending 481 response = self.client.get( 482 reverse("authentik_api:user-list"), 483 data={ 484 "ordering": "-last_updated", 485 }, 486 ) 487 self.assertEqual(response.status_code, 200) 488 489 body = loads(response.content) 490 self.assertEqual(len(body["results"]), 2) 491 self.assertEqual(body["results"][0]["pk"], admin.pk)
Test API sorting by last_updated
493 def test_sort_by_date_joined(self): 494 """Test API sorting by date_joined""" 495 User.objects.all().delete() 496 admin = create_test_admin_user() 497 self.client.force_login(admin) 498 499 user = create_test_user() 500 501 response = self.client.get( 502 reverse("authentik_api:user-list"), 503 data={ 504 "ordering": "date_joined", 505 }, 506 ) 507 self.assertEqual(response.status_code, 200) 508 509 body = loads(response.content) 510 self.assertEqual(len(body["results"]), 2) 511 self.assertEqual(body["results"][0]["pk"], admin.pk) 512 513 response = self.client.get( 514 reverse("authentik_api:user-list"), 515 data={ 516 "ordering": "-date_joined", 517 }, 518 ) 519 self.assertEqual(response.status_code, 200) 520 521 body = loads(response.content) 522 self.assertEqual(len(body["results"]), 2) 523 self.assertEqual(body["results"][0]["pk"], user.pk)
Test API sorting by date_joined
525 def test_service_account_validation_empty_username(self): 526 """Test service account creation with empty/blank username validation""" 527 self.client.force_login(self.admin) 528 529 # Test with empty string 530 response = self.client.post( 531 reverse("authentik_api:user-service-account"), 532 data={ 533 "name": "", 534 "create_group": True, 535 }, 536 ) 537 self.assertEqual(response.status_code, 400) 538 self.assertJSONEqual( 539 response.content, 540 {"name": ["This field may not be blank."]}, 541 ) 542 543 # Test with only whitespace 544 response = self.client.post( 545 reverse("authentik_api:user-service-account"), 546 data={ 547 "name": " ", 548 "create_group": True, 549 }, 550 ) 551 self.assertEqual(response.status_code, 400) 552 self.assertJSONEqual( 553 response.content, 554 {"name": ["This field may not be blank."]}, 555 ) 556 557 # Test with tab and newline characters 558 response = self.client.post( 559 reverse("authentik_api:user-service-account"), 560 data={ 561 "name": "\t\n", 562 "create_group": True, 563 }, 564 ) 565 self.assertEqual(response.status_code, 400) 566 self.assertJSONEqual( 567 response.content, 568 {"name": ["This field may not be blank."]}, 569 )
Test service account creation with empty/blank username validation
571 def test_service_account_validation_valid_username(self): 572 """Test service account creation with valid username""" 573 self.client.force_login(self.admin) 574 575 # Test with valid username 576 response = self.client.post( 577 reverse("authentik_api:user-service-account"), 578 data={ 579 "name": "valid-service-account", 580 "create_group": True, 581 }, 582 ) 583 self.assertEqual(response.status_code, 200) 584 585 # Verify response structure 586 body = loads(response.content) 587 self.assertIn("username", body) 588 self.assertIn("user_uid", body) 589 self.assertIn("user_pk", body) 590 self.assertIn("group_pk", body) # Should exist since create_group=True 591 self.assertIn("token", body) 592 593 # Verify field types 594 self.assertEqual(body["username"], "valid-service-account") 595 self.assertIsInstance(body["user_pk"], int) 596 self.assertIsInstance(body["user_uid"], str) 597 self.assertIsInstance(body["token"], str) 598 self.assertIsInstance(body["group_pk"], str)
Test service account creation with valid username
600 def test_service_account_validation_without_group(self): 601 """Test service account creation without creating a group""" 602 self.client.force_login(self.admin) 603 604 response = self.client.post( 605 reverse("authentik_api:user-service-account"), 606 data={ 607 "name": "no-group-service-account", 608 "create_group": False, 609 }, 610 ) 611 self.assertEqual(response.status_code, 200) 612 613 body = loads(response.content) 614 self.assertIn("username", body) 615 self.assertIn("user_uid", body) 616 self.assertIn("user_pk", body) 617 self.assertIn("token", body) 618 # Should NOT have group_pk when create_group=False 619 self.assertNotIn("group_pk", body)
Test service account creation without creating a group
621 def test_service_account_validation_duplicate_username(self): 622 """Test service account creation with duplicate username""" 623 self.client.force_login(self.admin) 624 625 # Create first service account 626 response = self.client.post( 627 reverse("authentik_api:user-service-account"), 628 data={ 629 "name": "duplicate-test", 630 "create_group": True, 631 }, 632 ) 633 self.assertEqual(response.status_code, 200) 634 635 # Attempt to create second with same username 636 response = self.client.post( 637 reverse("authentik_api:user-service-account"), 638 data={ 639 "name": "duplicate-test", 640 "create_group": True, 641 }, 642 ) 643 self.assertEqual(response.status_code, 400) 644 self.assertJSONEqual( 645 response.content, 646 {"name": ["This field must be unique."]}, 647 )
Test service account creation with duplicate username
649 def test_service_account_validation_invalid_create_group(self): 650 """Test service account creation with invalid create_group field""" 651 self.client.force_login(self.admin) 652 653 # Test with string instead of boolean 654 response = self.client.post( 655 reverse("authentik_api:user-service-account"), 656 data={ 657 "name": "test-sa", 658 "create_group": "invalid", 659 }, 660 ) 661 self.assertEqual(response.status_code, 400) 662 self.assertJSONEqual( 663 response.content, 664 {"create_group": ["Must be a valid boolean."]}, 665 ) 666 667 # Test with number instead of boolean 668 response = self.client.post( 669 reverse("authentik_api:user-service-account"), 670 data={ 671 "name": "test-sa", 672 "create_group": 123, 673 }, 674 ) 675 self.assertEqual(response.status_code, 400) 676 self.assertJSONEqual( 677 response.content, 678 {"create_group": ["Must be a valid boolean."]}, 679 )
Test service account creation with invalid create_group field
681 def test_service_account_validation_invalid_expiring(self): 682 """Test service account creation with invalid expiring field""" 683 self.client.force_login(self.admin) 684 685 # Test with string instead of boolean 686 response = self.client.post( 687 reverse("authentik_api:user-service-account"), 688 data={ 689 "name": "test-sa", 690 "expiring": "invalid", 691 }, 692 ) 693 self.assertEqual(response.status_code, 400) 694 self.assertJSONEqual( 695 response.content, 696 {"expiring": ["Must be a valid boolean."]}, 697 )
Test service account creation with invalid expiring field
699 def test_service_account_validation_invalid_expires(self): 700 """Test service account creation with invalid expires field""" 701 self.client.force_login(self.admin) 702 703 # Test with invalid datetime string 704 response = self.client.post( 705 reverse("authentik_api:user-service-account"), 706 data={ 707 "name": "test-sa", 708 "expires": "invalid-datetime", 709 }, 710 ) 711 self.assertEqual(response.status_code, 400) 712 self.assertJSONEqual( 713 response.content, 714 { 715 "expires": [ 716 "Datetime has wrong format. Use one of these formats instead: " 717 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 718 ] 719 }, 720 ) 721 722 # Test with invalid format 723 response = self.client.post( 724 reverse("authentik_api:user-service-account"), 725 data={ 726 "name": "test-sa", 727 "expires": "2024-13-45", # Invalid month/day 728 }, 729 ) 730 self.assertEqual(response.status_code, 400) 731 self.assertJSONEqual( 732 response.content, 733 { 734 "expires": [ 735 "Datetime has wrong format. Use one of these formats instead: " 736 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 737 ] 738 }, 739 )
Test service account creation with invalid expires field
741 def test_service_account_validation_multiple_errors(self): 742 """Test service account creation with multiple validation errors""" 743 self.client.force_login(self.admin) 744 745 response = self.client.post( 746 reverse("authentik_api:user-service-account"), 747 data={ 748 "name": "", # Empty username 749 "create_group": "invalid", # Invalid boolean 750 "expiring": 123, # Invalid boolean 751 "expires": "not-a-date", # Invalid datetime 752 }, 753 ) 754 self.assertEqual(response.status_code, 400) 755 self.assertJSONEqual( 756 response.content, 757 { 758 "name": ["This field may not be blank."], 759 "create_group": ["Must be a valid boolean."], 760 "expiring": ["Must be a valid boolean."], 761 "expires": [ 762 "Datetime has wrong format. Use one of these formats instead: " 763 "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]." 764 ], 765 }, 766 )
Test service account creation with multiple validation errors
768 def test_service_account_validation_user_friendly_duplicate_error(self): 769 """Test that duplicate username returns user-friendly error, not database error""" 770 self.client.force_login(self.admin) 771 772 # Create first service account 773 response = self.client.post( 774 reverse("authentik_api:user-service-account"), 775 data={ 776 "name": "duplicate-username-test", 777 "create_group": True, 778 }, 779 ) 780 self.assertEqual(response.status_code, 200) 781 782 # Attempt to create second with same username 783 response = self.client.post( 784 reverse("authentik_api:user-service-account"), 785 data={ 786 "name": "duplicate-username-test", 787 "create_group": True, 788 }, 789 ) 790 self.assertEqual(response.status_code, 400) 791 self.assertJSONEqual( 792 response.content, 793 {"name": ["This field must be unique."]}, 794 )
Test that duplicate username returns user-friendly error, not database error
796 def test_filter_last_login(self): 797 """Test API filtering by last_login""" 798 from datetime import timedelta 799 800 from django.utils import timezone 801 802 User.objects.all().delete() 803 admin = create_test_admin_user() 804 self.client.force_login(admin) 805 806 # Create users with different last_login values 807 user_recent = create_test_user() 808 user_recent.last_login = timezone.now() 809 user_recent.save() 810 811 user_old = create_test_user() 812 user_old.last_login = timezone.now() - timedelta(days=400) # Over 1 year ago 813 user_old.save() 814 815 user_never = create_test_user() 816 user_never.last_login = None # Never logged in 817 user_never.save() 818 819 # Filter users who logged in before 1 year ago 820 one_year_ago = (timezone.now() - timedelta(days=365)).isoformat() 821 response = self.client.get( 822 reverse("authentik_api:user-list"), 823 data={"last_login__lt": one_year_ago}, 824 ) 825 self.assertEqual(response.status_code, 200) 826 body = loads(response.content) 827 self.assertEqual(len(body["results"]), 1) 828 self.assertEqual(body["results"][0]["pk"], user_old.pk) 829 830 # Filter users who have never logged in 831 response = self.client.get( 832 reverse("authentik_api:user-list"), 833 data={"last_login__isnull": True}, 834 ) 835 self.assertEqual(response.status_code, 200) 836 body = loads(response.content) 837 # Should include user_never and admin (who hasn't logged in via the app) 838 pks = [r["pk"] for r in body["results"]] 839 self.assertIn(user_never.pk, pks)
Test API filtering by last_login
841 def test_sort_by_last_login(self): 842 """Test API sorting by last_login""" 843 from datetime import timedelta 844 845 from django.utils import timezone 846 847 User.objects.all().delete() 848 admin = create_test_admin_user() 849 self.client.force_login(admin) 850 851 user1 = create_test_user() 852 user1.last_login = timezone.now() - timedelta(days=10) 853 user1.save() 854 855 user2 = create_test_user() 856 user2.last_login = timezone.now() - timedelta(days=5) 857 user2.save() 858 859 # Ascending order (oldest first) 860 response = self.client.get( 861 reverse("authentik_api:user-list"), 862 data={"ordering": "last_login"}, 863 ) 864 self.assertEqual(response.status_code, 200) 865 body = loads(response.content) 866 # Users with null last_login come first, then user1 (older), then user2 (newer) 867 self.assertEqual(len(body["results"]), 3) 868 869 # Descending order (newest first) 870 response = self.client.get( 871 reverse("authentik_api:user-list"), 872 data={"ordering": "-last_login"}, 873 ) 874 self.assertEqual(response.status_code, 200) 875 body = loads(response.content) 876 # user2 should come before user1 (more recent login) 877 pks = [r["pk"] for r in body["results"]] 878 self.assertIn(user1.pk, pks) 879 self.assertIn(user2.pk, pks) 880 # Verify user2 comes before user1 in descending order 881 self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
Test API sorting by last_login