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 5388cee496 [#11104] test(client-python): Add integration tests for 
Owner API (#11105)
5388cee496 is described below

commit 5388cee49616852d0df886a47c511c7b47e191a7
Author: Sun Yuhan <[email protected]>
AuthorDate: Mon May 18 14:04:15 2026 +0800

    [#11104] test(client-python): Add integration tests for Owner API (#11105)
    
    ### What changes were proposed in this pull request?
    
    Add integration tests for the Python client's Owner API (`get_owner` /
    `set_owner`). Also fix `OwnerDTO` to handle case-insensitive
    deserialization — the server returns lowercase owner type values (e.g.,
    `"user"`) which caused `ValueError` during deserialization.
    
    ### Why are the changes needed?
    
    The Owner API only had unit tests with mocked HTTP responses.
    Integration tests are needed to catch runtime issues against a real
    Gravitino server with authorization enabled (e.g., the lowercase type
    deserialization bug).
    
    Fix: #11104
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    - 4 integration tests: get/set owner on metalake, catalog + schema
    level, error handling
    - 30 existing unit tests still pass
    - pylint 10.00/10
    
    ---------
    
    Co-authored-by: Sun Yuhan <[email protected]>
    Co-authored-by: Jerry Shao <[email protected]>
---
 .../gravitino/dto/authorization/owner_dto.py       |   7 +-
 .../client-python/tests/integration/test_owner.py  | 198 +++++++++++++++++++++
 .../client-python/tests/unittests/test_owner.py    |  12 ++
 3 files changed, 216 insertions(+), 1 deletion(-)

diff --git a/clients/client-python/gravitino/dto/authorization/owner_dto.py 
b/clients/client-python/gravitino/dto/authorization/owner_dto.py
index a68cd6abb1..bf52307d13 100644
--- a/clients/client-python/gravitino/dto/authorization/owner_dto.py
+++ b/clients/client-python/gravitino/dto/authorization/owner_dto.py
@@ -28,7 +28,12 @@ class OwnerDTO(Owner):
     """Represents an Owner Data Transfer Object (DTO)."""
 
     _name: str = field(metadata=config(field_name="name"))
-    _type: Owner.Type = field(metadata=config(field_name="type"))
+    _type: Owner.Type = field(
+        metadata=config(
+            field_name="type",
+            decoder=lambda v: Owner.Type(v.upper()) if isinstance(v, str) else 
v,
+        )
+    )
 
     def name(self) -> str:
         return self._name
diff --git a/clients/client-python/tests/integration/test_owner.py 
b/clients/client-python/tests/integration/test_owner.py
new file mode 100644
index 0000000000..200724395a
--- /dev/null
+++ b/clients/client-python/tests/integration/test_owner.py
@@ -0,0 +1,198 @@
+# 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,
+    Catalog,
+)
+from gravitino.api.authorization.owner import Owner
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
+from gravitino.exceptions.base import (
+    GravitinoRuntimeException,
+    NoSuchMetadataObjectException,
+    NotFoundException,
+)
+
+from tests.integration.integration_test_env import IntegrationTestEnv
+
+logger = logging.getLogger(__name__)
+
+
+class TestOwner(IntegrationTestEnv):
+    metalake_name: str = "test_owner_metalake" + str(randint(1, 10000))
+    catalog_name: str = "test_owner_catalog" + str(randint(1, 10000))
+    test_user: str = "test_owner_user"
+
+    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 create_catalog(self, catalog_name) -> Catalog:
+        return self.gravitino_client.create_catalog(
+            name=catalog_name,
+            catalog_type=Catalog.Type.FILESET,
+            provider="hadoop",
+            comment="test owner catalog",
+            properties={"location": "/tmp/test_owner"},
+        )
+
+    def clean_test_data(self):
+        self.gravitino_client = GravitinoClient(
+            uri="http://localhost:8090";, metalake_name=self.metalake_name
+        )
+        try:
+            self.gravitino_client.drop_catalog(name=self.catalog_name, 
force=True)
+        except GravitinoRuntimeException:
+            logger.warning("Failed to drop catalog %s", self.catalog_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_get_owner_metalake(self):
+        metalake_obj = MetadataObjects.of(
+            [self.metalake_name], MetadataObject.Type.METALAKE
+        )
+        owner = self.gravitino_client.get_owner(metalake_obj)
+        self.assertIsNotNone(owner)
+        self.assertTrue(len(owner.name()) > 0)
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+    def test_set_owner_metalake(self):
+        self.gravitino_client.add_user(self.test_user)
+
+        metalake_obj = MetadataObjects.of(
+            [self.metalake_name], MetadataObject.Type.METALAKE
+        )
+        self.gravitino_client.set_owner(
+            metalake_obj, self.test_user, Owner.Type.USER
+        )
+
+        owner = self.gravitino_client.get_owner(metalake_obj)
+        self.assertIsNotNone(owner)
+        self.assertEqual(self.test_user, owner.name())
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+    def test_owner_catalog_and_schema(self):
+        catalog = self.create_catalog(self.catalog_name)
+        self.gravitino_client.add_user(self.test_user)
+
+        # Test catalog-level owner
+        catalog_obj = MetadataObjects.of(
+            [self.catalog_name], MetadataObject.Type.CATALOG
+        )
+        owner = self.gravitino_client.get_owner(catalog_obj)
+        self.assertIsNotNone(owner)
+        self.assertTrue(len(owner.name()) > 0)
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+        self.gravitino_client.set_owner(
+            catalog_obj, self.test_user, Owner.Type.USER
+        )
+        owner = self.gravitino_client.get_owner(catalog_obj)
+        self.assertEqual(self.test_user, owner.name())
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+        # Test schema-level owner
+        schema_name = "test_owner_schema"
+        catalog.as_schemas().create_schema(schema_name, "comment", {})
+
+        schema_obj = MetadataObjects.of(
+            [self.catalog_name, schema_name], MetadataObject.Type.SCHEMA
+        )
+        self.gravitino_client.set_owner(
+            schema_obj, self.test_user, Owner.Type.USER
+        )
+
+        owner = self.gravitino_client.get_owner(schema_obj)
+        self.assertIsNotNone(owner)
+        self.assertEqual(self.test_user, owner.name())
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+    def test_owner_not_found(self):
+        # Get owner for a non-existent catalog
+        fake_catalog_obj = MetadataObjects.of(
+            ["non_existent_catalog"], MetadataObject.Type.CATALOG
+        )
+        with self.assertRaises(NoSuchMetadataObjectException):
+            self.gravitino_client.get_owner(fake_catalog_obj)
+
+        # Set owner with a non-existent user
+        metalake_obj = MetadataObjects.of(
+            [self.metalake_name], MetadataObject.Type.METALAKE
+        )
+        with self.assertRaises(NotFoundException):
+            self.gravitino_client.set_owner(
+                metalake_obj, "non_existent_user", Owner.Type.USER
+            )
diff --git a/clients/client-python/tests/unittests/test_owner.py 
b/clients/client-python/tests/unittests/test_owner.py
index 2da3b78487..5dcc35bb91 100644
--- a/clients/client-python/tests/unittests/test_owner.py
+++ b/clients/client-python/tests/unittests/test_owner.py
@@ -280,6 +280,18 @@ class TestOwnerDTOSerialization(unittest.TestCase):
         self.assertEqual("bob", owner.name())
         self.assertEqual(Owner.Type.GROUP, owner.type())
 
+    def test_owner_dto_deserialize_lowercase(self):
+        json_str = _json.dumps({"name": "alice", "type": "user"})
+        owner = OwnerDTO.from_json(json_str)
+        self.assertEqual("alice", owner.name())
+        self.assertEqual(Owner.Type.USER, owner.type())
+
+    def test_owner_dto_deserialize_lowercase_group(self):
+        json_str = _json.dumps({"name": "admin_group", "type": "group"})
+        owner = OwnerDTO.from_json(json_str)
+        self.assertEqual("admin_group", owner.name())
+        self.assertEqual(Owner.Type.GROUP, owner.type())
+
     def test_owner_dto_round_trip(self):
         original = OwnerDTO(_name="alice", _type=Owner.Type.USER)
         json_str = original.to_json()

Reply via email to