unsloth/studio/backend/tests/test_change_password_policy.py
Nilay 52609fb890
Studio: reset-password rotates the credential in place instead of deleting auth.db (#7573)
* reset-password: rotate the admin credential in place instead of deleting auth.db

* reset-password: fix the CI callers and error handling for the in-place rotation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* reset-password: narrow the CI change to the jobs that read .bootstrap_password

* reset-password: stop over-claiming what the reset revokes and when it takes effect

* auth: bind token issuance to the credential version that was verified

* auth: bind credential-creating writes to the version the request authenticated with

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* auth: bind the change-password and workflow-key writes to their own credential version

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* auth: read the credential version inside the transaction that validated it

* data-recipe: answer 401 when a reset revokes the credential mid job start

* Fix lint blocker and Windows path assertion for PR #7573

Drop the now-unused validate_api_key import from studio/backend/auth/authentication.py.
Every call site moved to validate_api_key_with_credential, so the Source lint job's
import-hoist gate flagged it as a blocker. The wrapper itself stays in storage.py;
test_api_key_expiry.py still exercises it.

Make test_run_reexec_forwards_resolved_frontend_on_public_launch compare against
str(Path(...)) instead of a POSIX literal. _find_frontend_dist returns a Path, so on
Windows the forwarded value is \fake\studio\frontend\dist and the assertion could
never pass there. Pre-existing, surfaced by running unsloth_cli/tests on Windows.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
2026-07-29 01:40:12 -07:00

77 lines
2.5 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import asyncio
import importlib.util
import sys
from pathlib import Path
import pytest
from fastapi import HTTPException
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(_BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(_BACKEND_ROOT))
from models.auth import ChangePasswordRequest # noqa: E402
# Load routes/auth.py directly so collection does not execute routes/__init__.py,
# which pulls in the heavy training/models/inference routers.
_route_path = _BACKEND_ROOT / "routes" / "auth.py"
_spec = importlib.util.spec_from_file_location("_change_password_route", _route_path)
assert _spec is not None and _spec.loader is not None
auth_routes = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(auth_routes)
@pytest.fixture
def _user(monkeypatch):
monkeypatch.setattr(
auth_routes.storage,
"get_user_and_secret",
lambda username: ("salt", "hash", "jwt-secret", False),
)
monkeypatch.setattr(
auth_routes.hashing,
"verify_password",
lambda password, salt, pwd_hash: password == "bootstrap-pw",
)
def _change(new_password):
payload = ChangePasswordRequest(
current_password = "bootstrap-pw",
new_password = new_password,
)
return asyncio.run(auth_routes.change_password(payload, None, "unsloth"))
def test_rejects_whitespace_only_password(_user):
with pytest.raises(HTTPException) as excinfo:
_change(" " * 8)
assert excinfo.value.status_code == 400
assert "spaces" in excinfo.value.detail
def test_rejects_tabs_and_spaces_password(_user):
with pytest.raises(HTTPException) as excinfo:
_change(" \t \t \t \t ")
assert excinfo.value.status_code == 400
def test_rejects_password_containing_spaces(_user):
with pytest.raises(HTTPException) as excinfo:
_change("correct horse battery")
assert excinfo.value.status_code == 400
assert "spaces" in excinfo.value.detail
def test_allows_password_without_spaces(_user, monkeypatch):
monkeypatch.setattr(
auth_routes.storage, "update_password", lambda *args, **kwargs: "rotated-secret"
)
monkeypatch.setattr(auth_routes, "create_access_token", lambda subject, **kwargs: "at")
monkeypatch.setattr(auth_routes, "create_refresh_token", lambda subject, **kwargs: "rt")
token = _change("correct-horse-battery")
assert token.access_token == "at"
assert token.must_change_password is False