# #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