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)
366 lines
12 KiB
Python
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
|