This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 5880e6972a [#11114] test(client-python): add integration tests for
User API and fix UserDTO audit validation (#11115)
5880e6972a is described below
commit 5880e6972ad040a2dbc05959d54b2344723ac482
Author: Sun Yuhan <[email protected]>
AuthorDate: Wed May 20 19:45:36 2026 +0800
[#11114] test(client-python): add integration tests for User API and fix
UserDTO audit validation (#11115)
### What changes were proposed in this pull request?
- Add audit null validation in `UserDTO.Builder.build()` to align with
the Java side
- Add integration tests for User API (`add_user`, `get_user`,
`remove_user`, `list_users`, `list_user_names`)
- Update existing unit tests to provide audit in Builder calls
### Why are the changes needed?
The Java `UserDTO.Builder.build()` validates `audit != null`, but the
Python counterpart does not. Also, PR #11058 only added mocked unit
tests for the User API. Integration tests are needed to validate against
a real Gravitino server.
Fix: #11114
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
- 8 new integration tests passed against a real Gravitino server with
authorization enabled
- 36 existing unit tests passed
---------
Co-authored-by: Sun Yuhan <[email protected]>
Co-authored-by: Jerry Shao <[email protected]>
---
.../gravitino/dto/authorization/user_dto.py | 2 +
.../client-python/tests/integration/test_user.py | 147 +++++++++++++++++++++
.../unittests/dto/responses/test_user_response.py | 21 ++-
.../tests/unittests/dto/test_user_dto.py | 43 +++++-
4 files changed, 206 insertions(+), 7 deletions(-)
diff --git a/clients/client-python/gravitino/dto/authorization/user_dto.py
b/clients/client-python/gravitino/dto/authorization/user_dto.py
index da32198534..0178f0702d 100644
--- a/clients/client-python/gravitino/dto/authorization/user_dto.py
+++ b/clients/client-python/gravitino/dto/authorization/user_dto.py
@@ -88,4 +88,6 @@ class UserDTO(User):
def build(self) -> UserDTO:
if not self._name:
raise ValueError("name cannot be null or empty")
+ if self._audit is None:
+ raise ValueError("audit cannot be null")
return UserDTO(self._name, self._roles, self._audit)
diff --git a/clients/client-python/tests/integration/test_user.py
b/clients/client-python/tests/integration/test_user.py
new file mode 100644
index 0000000000..9576461214
--- /dev/null
+++ b/clients/client-python/tests/integration/test_user.py
@@ -0,0 +1,147 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import logging
+import os
+from random import randint
+
+from gravitino import GravitinoAdminClient, GravitinoClient
+from gravitino.exceptions.base import (
+ GravitinoRuntimeException,
+ NoSuchUserException,
+ UserAlreadyExistsException,
+)
+
+from tests.integration.integration_test_env import IntegrationTestEnv
+
+logger = logging.getLogger(__name__)
+
+
+class TestUser(IntegrationTestEnv):
+ metalake_name: str = "test_user_metalake" + str(randint(1, 10000))
+
+ gravitino_admin_client: GravitinoAdminClient = None
+ gravitino_client: GravitinoClient = None
+
+ @classmethod
+ def setUpClass(cls):
+ cls._get_gravitino_home()
+ conf_path = os.path.join(cls.gravitino_home, "conf", "gravitino.conf")
+ cls._reset_conf({"gravitino.authorization.enable": "true"}, conf_path)
+ cls._append_conf({"gravitino.authorization.enable": "true"}, conf_path)
+ if (
+ os.environ.get("START_EXTERNAL_GRAVITINO") is not None
+ and os.environ.get("START_EXTERNAL_GRAVITINO").lower() == "true"
+ ):
+ cls.restart_server()
+ else:
+ super().setUpClass()
+ cls.gravitino_admin_client =
GravitinoAdminClient(uri="http://localhost:8090")
+
+ @classmethod
+ def tearDownClass(cls):
+ conf_path = os.path.join(cls.gravitino_home, "conf", "gravitino.conf")
+ cls._reset_conf({"gravitino.authorization.enable": "false"}, conf_path)
+ if (
+ os.environ.get("START_EXTERNAL_GRAVITINO") is not None
+ and os.environ.get("START_EXTERNAL_GRAVITINO").lower() == "true"
+ ):
+ cls.restart_server()
+ else:
+ super().tearDownClass()
+
+ def setUp(self):
+ self.init_test_env()
+
+ def tearDown(self):
+ self.clean_test_data()
+
+ def init_test_env(self):
+ self.gravitino_admin_client.create_metalake(
+ self.metalake_name, comment="", properties={}
+ )
+ self.gravitino_client = GravitinoClient(
+ uri="http://localhost:8090", metalake_name=self.metalake_name
+ )
+
+ def clean_test_data(self):
+ self.gravitino_client = GravitinoClient(
+ uri="http://localhost:8090", metalake_name=self.metalake_name
+ )
+ try:
+ self.gravitino_admin_client.drop_metalake(self.metalake_name,
force=True)
+ except GravitinoRuntimeException:
+ logger.warning("Failed to drop metalake %s", self.metalake_name)
+
+ def test_add_user(self):
+ user = self.gravitino_client.add_user("test_add_user")
+
+ self.assertIsNotNone(user)
+ self.assertEqual("test_add_user", user.name())
+ self.assertIsNotNone(user.audit_info())
+
+ def test_get_user(self):
+ self.gravitino_client.add_user("test_get_user")
+
+ user = self.gravitino_client.get_user("test_get_user")
+
+ self.assertIsNotNone(user)
+ self.assertEqual("test_get_user", user.name())
+
+ def test_list_users(self):
+ self.gravitino_client.add_user("test_list_user_1")
+ self.gravitino_client.add_user("test_list_user_2")
+
+ users = self.gravitino_client.list_users()
+
+ self.assertIsNotNone(users)
+ user_names = [u.name() for u in users]
+ self.assertIn("test_list_user_1", user_names)
+ self.assertIn("test_list_user_2", user_names)
+
+ def test_list_user_names(self):
+ self.gravitino_client.add_user("test_list_names_1")
+ self.gravitino_client.add_user("test_list_names_2")
+
+ names = self.gravitino_client.list_user_names()
+
+ self.assertIsNotNone(names)
+ self.assertIn("test_list_names_1", names)
+ self.assertIn("test_list_names_2", names)
+
+ def test_remove_user(self):
+ self.gravitino_client.add_user("test_remove_user")
+
+ removed = self.gravitino_client.remove_user("test_remove_user")
+ self.assertTrue(removed)
+
+ with self.assertRaises(NoSuchUserException):
+ self.gravitino_client.get_user("test_remove_user")
+
+ def test_get_nonexistent_user(self):
+ with self.assertRaises(NoSuchUserException):
+ self.gravitino_client.get_user("nonexistent_user")
+
+ def test_add_duplicate_user(self):
+ self.gravitino_client.add_user("test_duplicate_user")
+
+ with self.assertRaises(UserAlreadyExistsException):
+ self.gravitino_client.add_user("test_duplicate_user")
+
+ def test_remove_nonexistent_user(self):
+ removed = self.gravitino_client.remove_user("nonexistent_user")
+ self.assertFalse(removed)
diff --git
a/clients/client-python/tests/unittests/dto/responses/test_user_response.py
b/clients/client-python/tests/unittests/dto/responses/test_user_response.py
index e1070c63b7..1f55b544f4 100644
--- a/clients/client-python/tests/unittests/dto/responses/test_user_response.py
+++ b/clients/client-python/tests/unittests/dto/responses/test_user_response.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import json as _json
import unittest
+from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.authorization.user_dto import UserDTO
from gravitino.dto.responses.user_response import (
UserListResponse,
@@ -30,7 +31,11 @@ from gravitino.dto.responses.user_response import (
class TestUserResponses(unittest.TestCase):
def test_user_response(self):
- user_dto = UserDTO.builder().with_name("user1").build()
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
+ user_dto =
UserDTO.builder().with_name("user1").with_audit(audit).build()
resp = UserResponse(0, user_dto)
resp.validate()
@@ -62,8 +67,18 @@ class TestUserResponses(unittest.TestCase):
self.assertListEqual(names, deser_dict["names"])
def test_user_list_response(self):
- user1 = UserDTO.builder().with_name("user1").with_roles(["r1"]).build()
- user2 = UserDTO.builder().with_name("user2").build()
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
+ user1 = (
+ UserDTO.builder()
+ .with_name("user1")
+ .with_roles(["r1"])
+ .with_audit(audit)
+ .build()
+ )
+ user2 = UserDTO.builder().with_name("user2").with_audit(audit).build()
resp = UserListResponse(0, [user1, user2])
diff --git a/clients/client-python/tests/unittests/dto/test_user_dto.py
b/clients/client-python/tests/unittests/dto/test_user_dto.py
index 74bf2b2e10..ef921516c3 100644
--- a/clients/client-python/tests/unittests/dto/test_user_dto.py
+++ b/clients/client-python/tests/unittests/dto/test_user_dto.py
@@ -44,7 +44,13 @@ class TestUserDTO(unittest.TestCase):
self.assertEqual(deser_dict["audit"]["creator"], "admin")
def test_user_dto_without_roles(self):
- user_dto = UserDTO.builder().with_name("test_user_no_roles").build()
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
+ user_dto = (
+
UserDTO.builder().with_name("test_user_no_roles").with_audit(audit).build()
+ )
ser_json = _json.dumps(user_dto.to_dict()).encode("utf-8")
deser_dict = _json.loads(ser_json)
@@ -52,20 +58,29 @@ class TestUserDTO(unittest.TestCase):
self.assertEqual(deser_dict["roles"], [])
def test_user_dto_methods(self):
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
user_dto = (
UserDTO.builder()
.with_name("method_user")
.with_roles(["admin_role"])
+ .with_audit(audit)
.build()
)
self.assertEqual(user_dto.name(), "method_user")
self.assertEqual(user_dto.roles(), ["admin_role"])
- self.assertIsNone(user_dto.audit_info())
+ self.assertIsNotNone(user_dto.audit_info())
def test_builder_empty_name_raises(self):
with self.assertRaises(ValueError):
UserDTO.builder().build()
+ def test_builder_null_audit_raises(self):
+ with self.assertRaises(ValueError):
+ UserDTO.builder().with_name("test_user").build()
+
def test_equality_and_hash(self):
audit = AuditDTO(_creator="test", _create_time="2024-01-01T00:00:00Z")
@@ -96,11 +111,31 @@ class TestUserDTO(unittest.TestCase):
self.assertNotEqual(user1, user3)
def test_roles_returns_list_type(self):
- user_dto =
UserDTO.builder().with_name("test").with_roles(["r1"]).build()
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
+ user_dto = (
+ UserDTO.builder()
+ .with_name("test")
+ .with_roles(["r1"])
+ .with_audit(audit)
+ .build()
+ )
self.assertIsInstance(user_dto.roles(), list)
def test_roles_immutability(self):
- user_dto = UserDTO.builder().with_name("test").with_roles(["r1",
"r2"]).build()
+ audit = AuditDTO(
+ _creator="admin",
+ _create_time="2024-01-01T00:00:00Z",
+ )
+ user_dto = (
+ UserDTO.builder()
+ .with_name("test")
+ .with_roles(["r1", "r2"])
+ .with_audit(audit)
+ .build()
+ )
roles = user_dto.roles()
roles.append("r3")
self.assertEqual(["r1", "r2"], user_dto.roles())