-
Notifications
You must be signed in to change notification settings - Fork 165
test(BA-3589): add tests for manager's auth api handler #7653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive unit tests for the validate_ip function and authentication decorators (auth_required, admin_required, superadmin_required) in preparation for migrating api/auth.py to follow the pydantic model. The tests establish a baseline to ensure safe refactoring with regression detection.
Key Changes
- Added test coverage for IP validation logic (allow/deny scenarios)
- Added test coverage for authentication decorator behavior and authorization checks
- Added tests to verify decorator attributes are properly set
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def test_validate_ip_allowed() -> None: | ||
| """IP within CIDR range should be allowed, empty allowlist allows all.""" | ||
| request = MagicMock(spec=web.Request) | ||
| request.headers = {"X-Forwarded-For": "10.0.0.50"} | ||
| request.remote = None | ||
|
|
||
| # Empty allowlist allows all IPs | ||
| user: dict[str, Any] = {"allowed_client_ip": None} | ||
| validate_ip(request, user) | ||
|
|
||
| # IP within CIDR range is allowed | ||
| user = {"allowed_client_ip": [ReadableCIDR("10.0.0.0/24", is_network=True)]} | ||
| validate_ip(request, user) | ||
|
|
||
|
|
||
| def test_validate_ip_denied() -> None: | ||
| """IP not in allowlist should raise AuthorizationFailed.""" | ||
| request = MagicMock(spec=web.Request) | ||
| request.headers = {"X-Forwarded-For": "192.168.1.100"} | ||
| request.remote = None | ||
|
|
||
| user: dict[str, Any] = {"allowed_client_ip": [ReadableCIDR("10.0.0.0/24", is_network=True)]} | ||
| with pytest.raises(AuthorizationFailed, match="not allowed IP address"): | ||
| validate_ip(request, user) |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage for validate_ip is incomplete. Several edge cases from the implementation are not tested:
- Invalid IP address format - should raise InvalidAuthParameters
- Missing client IP (both X-Forwarded-For and request.remote are None) - should raise AuthorizationFailed with "Not allowed IP address"
- Non-list allowed_client_ip value - should raise InvalidClientIPConfig
- Empty list allowed_client_ip behavior (currently only None is tested)
These test cases should be added to ensure comprehensive coverage before the migration mentioned in the PR description.
| from ai.backend.manager.errors.auth import ( | ||
| AuthorizationFailed, | ||
| InvalidAuthParameters, | ||
| ) |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The InvalidClientIPConfig exception is not imported but should be included to test the edge case where allowed_client_ip is not a list (line 397-398 in auth.py). Add this import to the error imports section to enable comprehensive test coverage.
| @pytest.mark.asyncio | ||
| async def test_superadmin_required_sets_handler_attr(self) -> None: | ||
| """superadmin_required should set handler attributes.""" | ||
|
|
||
| @superadmin_required | ||
| async def handler(request: web.Request) -> web.Response: | ||
| return web.Response(text="ok") | ||
|
|
||
| # get_handler_attr expects request.match_info.handler, so we check _backend_attrs directly | ||
| attrs = getattr(handler, "_backend_attrs", {}) | ||
| assert attrs.get("auth_required", False) is True | ||
| assert attrs.get("auth_scope", None) == "superadmin" |
Copilot
AI
Dec 31, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test case for superadmin_required when the request is not authorized (is_authorized=False). Currently, only the cases where is_authorized=True with is_superadmin=True and is_superadmin=False are tested. Add a test case for when is_authorized=False to ensure complete coverage of the decorator's authorization checks, similar to test_admin_required_unauthorized.
HyeockJinKim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easier to check the implementation details while writing test code, rather than focusing on those details.
Please write test code prioritizing what behavior should be satisfied.
Additionally, please include the JIRA issue number in the PR title under "test:" sections. (e.g. test(BA-...): ...)
|
And the timeline-check part in CI got stuck, please check how it applies to other PRs and changes/directories. (Refer to https://github.com/lablup/backend.ai?tab=contributing-ov-file#4-write-changelog-fragment-towncrier.) |
9552aac to
4af738e
Compare
ff73214 to
1ef5bcb
Compare
HyeockJinKim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned that there are too many utility functions in this work. Ideally, we should keep test-related objects with simpler mocking objects, rather than multiple implementations. (Considering using aiohttp's Fixture as well.)
|
Thanks for the review! I thought the same thing while writing the code, but at the same time, I thought it will be easier to test other If I minimize utility functions but keep |
|
Or I can fix it to look something more like |
1ef5bcb to
30ccaff
Compare
|
This commit implements minimalized mocking utility for test auth.py, referenced vfolder test scenarios, i think we can move |
HyeockJinKim
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to create a fixture that registers each handler to the application and sends a request to verify it. Please take a look at https://docs.aiohttp.org/en/stable/testing.html#unittest for reference.
Could I create a sample test and share it? @kwonkwonn
fregataa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we decide to implement fixture in a separate conftest file? @HyeockJinKim
yes, If it's not bothering you so much, i will be very appreciated! |
30ccaff to
4ab96da
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @pytest.mark.asyncio | ||
| async def test_returns_bad_request_on_failure( | ||
| self, | ||
| authorized_request: MagicMock, | ||
| mock_root_ctx: MagicMock, | ||
| ) -> None: | ||
| """Verify BAD_REQUEST is returned when password update fails.""" | ||
| user_uuid = uuid.uuid4() | ||
| authorized_request.text = AsyncMock( | ||
| return_value=json.dumps({ | ||
| "old_password": "oldpass", | ||
| "new_password": "newpass", | ||
| "new_password2": "differentpass", | ||
| }) | ||
| ) | ||
| authorized_request["user"] = { | ||
| "uuid": user_uuid, | ||
| "email": "[email protected]", | ||
| "domain_name": "default", | ||
| } | ||
| authorized_request["keypair"] = {"access_key": "AKTEST"} | ||
| mock_root_ctx.processors.auth.update_password.wait_for_complete = AsyncMock( | ||
| return_value=UpdatePasswordActionResult(success=False, message="mismatch") | ||
| ) | ||
|
|
||
| response = await update_password(authorized_request) | ||
|
|
||
| assert response.status == HTTPStatus.BAD_REQUEST |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test verifies that BAD_REQUEST status is returned on password update failure, but doesn't check the error message in the response body. The handler returns a specific error message 'new password mismatch' that should be verified to ensure the correct error information is provided to users.
fregataa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Please check the comment and check the hard coded values
| """Verify processor is called with correct params.""" | ||
| user_uuid = uuid.uuid4() | ||
| authorized_request.text = AsyncMock( | ||
| return_value=json.dumps({"email": "[email protected]", "full_name": "New Name"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using a variable for "full_name" rather than using a hard coded value? Because this should be tested by comparing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
modified "full_name" to be compared as variable.
thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is that we need to check all values that are compared for testing and change them to variables, not just the full_name case.
For example:
# OK
name = "hello"
result = func(name)
assert result.name == name
# NOT
result = func("hello")
assert result.name == "hello"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh i misunderstand that.
I'll fix it to be in variable form
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For any value which is subject of comparions, I move it out from function scope and move it aside to class fields.
which can increase flexability for future test scenarios.
thank you for concise review!
a82a84d to
0f73da8
Compare
fregataa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
0f73da8 to
5324bc3
Compare
|
Please rebase to main |
5324bc3 to
a5081cf
Compare
resolves #7624 (BA-3589)
Increase test coverage before migrating api/auth.py to follow pydantic model.
This Pr specifically contains 'validate_ip' and 'validate decorator' which will be used before and after migration of auth.py
Checklist: (if applicable)
ai.backend.testdocsdirectory