Files
ss-tools/backend/tests/schemas/test_auth.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
2026-06-15 13:55:57 +03:00

366 lines
12 KiB
Python

# #region Test.Schemas.Auth [C:2] [TYPE Module] [SEMANTICS test,schema,auth,validation]
# @BRIEF Tests for schemas/auth.py — Token, TokenData, PermissionSchema, RoleSchema,
# RoleCreate, RoleUpdate, ADGroupMappingSchema, ADGroupMappingCreate, UserBase,
# UserCreate, UserUpdate, User.
# @RELATION BINDS_TO -> [AuthSchemas]
# @TEST_EDGE: missing_field -> validation catches missing required fields
# @TEST_EDGE: invalid_type -> field_validators reject bad inputs
# @TEST_EDGE: serialization -> round-trip via model_dump/model_validate
# @TEST_EDGE: password_strength -> validator checks uppercase, lowercase, digit, length
from datetime import datetime, timezone
import pytest
from pydantic import ValidationError
from src.schemas.auth import (
ADGroupMappingCreate,
ADGroupMappingSchema,
PermissionSchema,
RoleCreate,
RoleSchema,
RoleUpdate,
Token,
TokenData,
User,
UserBase,
UserCreate,
UserUpdate,
)
class TestToken:
"""Token — access_token and token_type."""
def test_minimal(self):
t = Token(access_token="jwt", token_type="bearer")
assert t.access_token == "jwt"
assert t.token_type == "bearer"
def test_missing_raises(self):
with pytest.raises(ValidationError):
Token()
def test_serialize_roundtrip(self):
t = Token(access_token="tok", token_type="Bearer")
data = t.model_dump()
restored = Token.model_validate(data)
assert restored.access_token == "tok"
class TestTokenData:
"""TokenData — username optional, scopes default empty."""
def test_defaults(self):
td = TokenData()
assert td.username is None
assert td.scopes == []
def test_with_values(self):
td = TokenData(username="alice", scopes=["admin"])
assert td.username == "alice"
assert td.scopes == ["admin"]
def test_none_username(self):
td = TokenData(username=None)
assert td.username is None
def test_serialize_roundtrip(self):
td = TokenData(username="bob", scopes=["read", "write"])
data = td.model_dump()
restored = TokenData.model_validate(data)
assert restored.username == "bob"
assert "read" in restored.scopes
class TestPermissionSchema:
"""PermissionSchema — resource/action with optional id and from_attributes."""
def test_minimal(self):
p = PermissionSchema(resource="dashboard", action="READ")
assert p.resource == "dashboard"
assert p.action == "READ"
assert p.id is None
def test_with_id(self):
p = PermissionSchema(id="perm-1", resource="dataset", action="WRITE")
assert p.id == "perm-1"
def test_serialize_roundtrip(self):
p = PermissionSchema(resource="dashboard", action="READ")
data = p.model_dump()
restored = PermissionSchema.model_validate(data)
assert restored.resource == "dashboard"
def test_from_attributes(self):
"""ConfigDict(from_attributes=True) allows ORM mode."""
class FakeORM:
id = "perm-1"
resource = "dashboard"
action = "READ"
p = PermissionSchema.model_validate(FakeORM())
assert p.id == "perm-1"
assert p.resource == "dashboard"
class TestRoleSchema:
"""RoleSchema — role with nested permissions."""
def test_minimal(self):
r = RoleSchema(id="role-1", name="Admin")
assert r.description is None
assert r.permissions == []
def test_with_permissions(self):
perms = [PermissionSchema(resource="dashboard", action="READ")]
r = RoleSchema(id="role-1", name="Editor", description="Can edit", permissions=perms)
assert len(r.permissions) == 1
def test_serialize_roundtrip(self):
r = RoleSchema(id="role-1", name="Viewer")
data = r.model_dump()
restored = RoleSchema.model_validate(data)
assert restored.name == "Viewer"
def test_from_attributes(self):
class FakeORM:
id = "role-1"
name = "Admin"
description = "Full access"
permissions = []
r = RoleSchema.model_validate(FakeORM())
assert r.id == "role-1"
assert r.description == "Full access"
class TestRoleCreate:
"""RoleCreate — name required, optional description and permissions."""
def test_minimal(self):
r = RoleCreate(name="Editor")
assert r.description is None
assert r.permissions == []
def test_with_permissions(self):
r = RoleCreate(name="Admin", description="Full", permissions=["dashboard:READ", "dataset:WRITE"])
assert len(r.permissions) == 2
def test_missing_name_raises(self):
with pytest.raises(ValidationError):
RoleCreate()
def test_serialize_roundtrip(self):
r = RoleCreate(name="Viewer", permissions=["dashboard:READ"])
data = r.model_dump()
restored = RoleCreate.model_validate(data)
assert restored.name == "Viewer"
class TestRoleUpdate:
"""RoleUpdate — all fields optional for partial update."""
def test_defaults(self):
r = RoleUpdate()
assert r.name is None
assert r.description is None
assert r.permissions is None
def test_partial(self):
r = RoleUpdate(name="Updated")
assert r.name == "Updated"
assert r.description is None
def test_serialize_roundtrip(self):
r = RoleUpdate(name="New", description="desc", permissions=["a:READ"])
data = r.model_dump()
restored = RoleUpdate.model_validate(data)
assert restored.name == "New"
assert restored.description == "desc"
class TestADGroupMappingSchema:
"""ADGroupMappingSchema — id, ad_group, role_id with from_attributes."""
def test_minimal(self):
m = ADGroupMappingSchema(id="m-1", ad_group="DOMAIN\\Users", role_id="role-1")
assert m.ad_group == "DOMAIN\\Users"
def test_serialize_roundtrip(self):
m = ADGroupMappingSchema(id="m-1", ad_group="CN=Group,DC=corp", role_id="role-1")
data = m.model_dump()
restored = ADGroupMappingSchema.model_validate(data)
assert restored.ad_group == "CN=Group,DC=corp"
def test_from_attributes(self):
class FakeORM:
id = "m-1"
ad_group = "DOMAIN\\Users"
role_id = "role-1"
m = ADGroupMappingSchema.model_validate(FakeORM())
assert m.ad_group == "DOMAIN\\Users"
class TestADGroupMappingCreate:
"""ADGroupMappingCreate — ad_group with field_validator."""
def test_valid_ad_group(self):
m = ADGroupMappingCreate(ad_group="DOMAIN\\Users", role_id="role-1")
assert m.ad_group == "DOMAIN\\Users"
def test_valid_dn_format(self):
m = ADGroupMappingCreate(ad_group="CN=MyGroup,DC=example,DC=com", role_id="role-1")
assert "CN=MyGroup" in m.ad_group
def test_empty_ad_group_raises(self):
with pytest.raises(ValidationError, match="must not be empty"):
ADGroupMappingCreate(ad_group="", role_id="role-1")
def test_whitespace_only_raises(self):
with pytest.raises(ValidationError):
ADGroupMappingCreate(ad_group=" ", role_id="role-1")
def test_invalid_chars_raises(self):
with pytest.raises(ValidationError, match="invalid characters"):
ADGroupMappingCreate(ad_group="bad group!", role_id="role-1")
def test_strips_whitespace(self):
m = ADGroupMappingCreate(ad_group=" DOMAIN\\Users ", role_id="role-1")
assert m.ad_group == "DOMAIN\\Users"
def test_serialize_roundtrip(self):
m = ADGroupMappingCreate(ad_group="DOMAIN\\Users", role_id="role-1")
data = m.model_dump()
restored = ADGroupMappingCreate.model_validate(data)
assert restored.ad_group == "DOMAIN\\Users"
class TestUserBase:
"""UserBase — username, optional email, is_active default True."""
def test_minimal(self):
u = UserBase(username="alice")
assert u.email is None
assert u.is_active is True
def test_with_email(self):
u = UserBase(username="alice", email="alice@example.com")
assert u.email == "alice@example.com"
def test_inactive(self):
u = UserBase(username="bob", is_active=False)
assert u.is_active is False
def test_serialize_roundtrip(self):
u = UserBase(username="alice", email="a@b.com")
data = u.model_dump()
restored = UserBase.model_validate(data)
assert restored.username == "alice"
assert restored.email == "a@b.com"
class TestUserCreate:
"""UserCreate — extends UserBase with password validation and roles."""
def test_valid_password(self):
u = UserCreate(username="alice", password="ValidPass1")
assert u.password == "ValidPass1"
assert u.roles == []
def test_too_short_raises(self):
with pytest.raises(ValidationError, match="at least 8"):
UserCreate(username="alice", password="Ab1")
def test_no_uppercase_raises(self):
with pytest.raises(ValidationError, match="uppercase"):
UserCreate(username="alice", password="lowercase1")
def test_no_lowercase_raises(self):
with pytest.raises(ValidationError, match="lowercase"):
UserCreate(username="alice", password="UPPERCASE1")
def test_no_digit_raises(self):
with pytest.raises(ValidationError, match="digit"):
UserCreate(username="alice", password="Uppercase")
def test_with_roles(self):
u = UserCreate(username="alice", password="ValidPass1", roles=["admin"])
assert u.roles == ["admin"]
def test_serialize_roundtrip(self):
u = UserCreate(username="alice", password="ValidPass1", email="a@b.com")
data = u.model_dump()
restored = UserCreate.model_validate(data)
assert restored.username == "alice"
assert restored.email == "a@b.com"
class TestUserUpdate:
"""UserUpdate — all fields optional for partial update."""
def test_defaults(self):
u = UserUpdate()
assert u.email is None
assert u.password is None
assert u.is_active is None
assert u.roles is None
def test_partial_email(self):
u = UserUpdate(email="new@example.com")
assert u.email == "new@example.com"
def test_serialize_roundtrip(self):
u = UserUpdate(email="a@b.com", is_active=False)
data = u.model_dump()
restored = UserUpdate.model_validate(data)
assert restored.is_active is False
class TestUser:
"""User — full user response with id, auth_source, timestamps, roles."""
def make_user(self, **overrides):
ts = datetime.now(timezone.utc)
defaults = dict(
id="u-1", username="alice", auth_source="database",
created_at=ts, last_login=None, roles=[],
)
defaults.update(overrides)
return User(**defaults)
def test_required_fields(self):
u = self.make_user()
assert u.id == "u-1"
assert u.username == "alice"
assert u.auth_source == "database"
def test_with_last_login(self):
ts = datetime.now(timezone.utc)
u = self.make_user(last_login=ts)
assert u.last_login is not None
def test_with_roles(self):
role = RoleSchema(id="r-1", name="Admin")
u = self.make_user(roles=[role])
assert len(u.roles) == 1
assert u.roles[0].name == "Admin"
def test_serialize_roundtrip(self):
u = self.make_user()
data = u.model_dump()
restored = User.model_validate(data)
assert restored.id == "u-1"
assert restored.username == "alice"
def test_from_attributes(self):
class FakeORM:
id = "u-1"
username = "alice"
email = "a@b.com"
is_active = True
auth_source = "ldap"
created_at = datetime.now(timezone.utc)
last_login = None
roles = []
u = User.model_validate(FakeORM())
assert u.auth_source == "ldap"
# #endregion Test.Schemas.Auth