This is an automated email from the ASF dual-hosted git repository.

pradeepagrawal8184 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/master by this push:
     new 18880728e RANGER-5823: Adding public api test cases of policy and repo 
creation in docker based framework (#1287)
18880728e is described below

commit 18880728e7e8ed0630650658f774e28d684172d2
Author: Vishal Bhavsar <[email protected]>
AuthorDate: Wed Oct 7 18:20:48 2026 +0530

    RANGER-5823: Adding public api test cases of policy and repo creation in 
docker based framework (#1287)
---
 functional-tests/.gitignore                        |   8 +
 .../test_public_hive_services_and_policies_api.py  | 395 +++++++++++++++++++++
 functional-tests/pytest.ini                        |  10 +-
 functional-tests/ranger_test_utils/utils.py        | 358 +++++++++++++++++++
 functional-tests/readme.md                         |   9 +-
 functional-tests/run-tests.sh                      |   5 +-
 6 files changed, 779 insertions(+), 6 deletions(-)

diff --git a/functional-tests/.gitignore b/functional-tests/.gitignore
new file mode 100644
index 000000000..606df5c16
--- /dev/null
+++ b/functional-tests/.gitignore
@@ -0,0 +1,8 @@
+# Local Python / pytest (do not commit)
+__pycache__/
+
+# Virtualenv created by run-tests.sh
+myenv/
+
+# Pytest cache
+.pytest_cache/
diff --git a/functional-tests/api/test_public_hive_services_and_policies_api.py 
b/functional-tests/api/test_public_hive_services_and_policies_api.py
new file mode 100644
index 000000000..fd5ed008b
--- /dev/null
+++ b/functional-tests/api/test_public_hive_services_and_policies_api.py
@@ -0,0 +1,395 @@
+# 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 pytest
+
+from ranger_test_utils.utils import (
+    POLICY_URL,
+    SERVICE_URL,
+    assert_http_ok,
+    build_hive_policy_payload,
+    build_hive_service_payload,
+    configure_test_logging,
+    create_ranger_admin_session,
+    ensure_argus_policy_principals,
+    find_policy_by_id,
+    find_service_by_id,
+    get_test_logger,
+    log_http_response,
+    policy_matches,
+    service_matches,
+    unique_suffix,
+)
+
+pytestmark = pytest.mark.api
+
+logger = get_test_logger(__name__)
+
+http = None
+run_id = None
+
+repo_id = None
+policy_id = None
+service_payload = None
+policy_payload = None
+
+repo_id1 = None
+repo_id2 = None
+service_payload1 = None
+service_payload2 = None
+policy_id1 = None
+policy_id2 = None
+
+
+def setup_module():
+    global http, run_id
+
+    configure_test_logging(module_name=__name__)
+    logger.info("=== setup_module: start ===")
+
+    http = create_ranger_admin_session()
+    run_id = unique_suffix()
+    logger.info("Test run id: %s", run_id)
+
+    logger.info("Creating policy users and groups in Ranger Admin")
+    ensure_argus_policy_principals(http)
+
+    logger.info("=== setup_module: done ===")
+
+
+def teardown_module():
+    if http is None:
+        return
+
+    logger.info("=== teardown_module: cleaning leftover policies and services 
===")
+
+    if policy_id is not None:
+        logger.info("Deleting leftover policy id=%s", policy_id)
+        http.delete(POLICY_URL + str(policy_id))
+    if policy_id1 is not None:
+        logger.info("Deleting leftover policy id=%s", policy_id1)
+        http.delete(POLICY_URL + str(policy_id1))
+    if policy_id2 is not None:
+        logger.info("Deleting leftover policy id=%s", policy_id2)
+        http.delete(POLICY_URL + str(policy_id2))
+
+    if repo_id is not None:
+        logger.info("Deleting leftover service id=%s", repo_id)
+        http.delete(SERVICE_URL + str(repo_id))
+    if repo_id1 is not None:
+        logger.info("Deleting leftover service id=%s", repo_id1)
+        http.delete(SERVICE_URL + str(repo_id1))
+    if repo_id2 is not None:
+        logger.info("Deleting leftover service id=%s", repo_id2)
+        http.delete(SERVICE_URL + str(repo_id2))
+
+    logger.info("=== teardown_module: done ===")
+
+
+def test_create_repository_using_pub_api_02a():
+    global repo_id, service_payload
+
+    logger.info("--- test_create_repository_using_pub_api_02a ---")
+    service_name = "test_hive_repo_" + run_id
+    service_payload = build_hive_service_payload(
+        name=service_name,
+        description="hive repo using api",
+    )
+    logger.info("POST hive service name=%s", service_name)
+
+    response = http.post(SERVICE_URL, json=service_payload)
+    log_http_response(logger, "POST create service", response)
+    assert_http_ok(response, (200, 201), "POST create hive service", 
SERVICE_URL)
+
+    body = response.json()
+    repo_id = body["id"]
+    logger.info("Created service id=%s", repo_id)
+    assert service_matches(service_payload, body)
+
+
+def test_get_repo_using_pub_api_02b():
+    logger.info("--- test_get_repo_using_pub_api_02b ---")
+    url = SERVICE_URL + str(repo_id)
+    logger.info("GET service id=%s", repo_id)
+
+    response = http.get(url)
+    log_http_response(logger, "GET service", response)
+    assert_http_ok(response, 200, "GET hive service by id", url)
+
+    body = response.json()
+    assert body["id"] == repo_id
+    assert service_matches(service_payload, body)
+    logger.info("GET service OK")
+
+
+def test_update_repo_using_pub_api_02d():
+    global repo_id, service_payload
+
+    logger.info("--- test_update_repo_using_pub_api_02d ---")
+    service_name = "test_hive_repo_updating_" + run_id
+    service_payload = build_hive_service_payload(
+        name=service_name,
+        description="hive repo using api updating",
+    )
+    url = SERVICE_URL + str(repo_id)
+    logger.info("PUT service id=%s new name=%s", repo_id, service_name)
+
+    response = http.put(url, json=service_payload)
+    log_http_response(logger, "PUT service", response)
+    assert_http_ok(response, 200, "PUT update hive service", url)
+
+    body = response.json()
+    repo_id = body["id"]
+    assert service_matches(service_payload, body)
+    logger.info("Updated service id=%s", repo_id)
+
+
+def test_create_hive_policy_using_pub_api_02e():
+    global policy_id, policy_payload
+
+    logger.info("--- test_create_hive_policy_using_pub_api_02e ---")
+    policy_name = "test_hive_api_" + run_id
+    policy_payload = build_hive_policy_payload(
+        service_name=service_payload["name"],
+        policy_name=policy_name,
+        database_list="default,xatest",
+        table_list="sample01,sample02",
+        column_list="*",
+        perm_map_list=[
+            {"userList": ["hrt_21"], "permList": ["select", "update"]},
+            {"userList": ["hrt_22"], "permList": ["select"]},
+        ],
+    )
+    logger.info("POST policy name=%s for service=%s", policy_name, 
service_payload["name"])
+
+    response = http.post(POLICY_URL, json=policy_payload)
+    log_http_response(logger, "POST create policy", response)
+    assert_http_ok(response, (200, 201), "POST create hive policy", POLICY_URL)
+
+    body = response.json()
+    policy_id = body["id"]
+    logger.info("Created policy id=%s", policy_id)
+    assert policy_matches(policy_payload, body)
+
+
+def test_get_hive_policy_using_public_api_02f():
+    logger.info("--- test_get_hive_policy_using_public_api_02f ---")
+    url = POLICY_URL + str(policy_id)
+    logger.info("GET policy id=%s", policy_id)
+
+    response = http.get(url)
+    log_http_response(logger, "GET policy", response)
+    assert_http_ok(response, 200, "GET hive policy by id", url)
+
+    body = response.json()
+    assert body["id"] == policy_id
+    assert policy_matches(policy_payload, body)
+    logger.info("GET policy OK")
+
+
+def test_update_policy_using_pub_api_02h():
+    global policy_id, policy_payload
+
+    logger.info("--- test_update_policy_using_pub_api_02h ---")
+    policy_name = "test_hive_api_updated_" + run_id
+    policy_payload = build_hive_policy_payload(
+        service_name=service_payload["name"],
+        policy_name=policy_name,
+        database_list="default,xatest",
+        table_list="sample01,sample02",
+        column_list="*",
+        perm_map_list=[
+            {"groupList": ["audit"], "permList": ["select", "update"]},
+            {"userList": ["hrt_21"], "permList": ["select"]},
+        ],
+        policy_id=policy_id,
+    )
+    url = POLICY_URL + str(policy_id)
+    logger.info("PUT policy id=%s", policy_id)
+
+    response = http.put(url, json=policy_payload)
+    log_http_response(logger, "PUT policy", response)
+    assert_http_ok(response, 200, "PUT update hive policy", url)
+
+    body = response.json()
+    policy_id = body["id"]
+    assert policy_matches(policy_payload, body)
+    logger.info("Updated policy id=%s", policy_id)
+
+
+def test_delete_policy_using_pub_api_02i():
+    global policy_id
+
+    logger.info("--- test_delete_policy_using_pub_api_02i ---")
+    url = POLICY_URL + str(policy_id)
+    logger.info("DELETE policy id=%s", policy_id)
+
+    delete_response = http.delete(url)
+    log_http_response(logger, "DELETE policy", delete_response)
+    assert_http_ok(delete_response, (200, 204), "DELETE hive policy", url)
+
+    get_response = http.get(url)
+    log_http_response(logger, "GET policy after delete", get_response)
+    assert_http_ok(get_response, (400, 404), "GET policy after delete (expect 
missing)", url)
+
+    policy_id = None
+    logger.info("Policy deleted")
+
+
+def test_delete_repo_using_pub_api_02j():
+    global repo_id
+
+    logger.info("--- test_delete_repo_using_pub_api_02j ---")
+    url = SERVICE_URL + str(repo_id)
+    logger.info("DELETE service id=%s", repo_id)
+
+    delete_response = http.delete(url)
+    log_http_response(logger, "DELETE service", delete_response)
+    assert_http_ok(delete_response, (200, 204), "DELETE hive service", url)
+
+    get_response = http.get(url)
+    log_http_response(logger, "GET service after delete", get_response)
+    assert_http_ok(get_response, (400, 404), "GET service after delete (expect 
missing)", url)
+
+    repo_id = None
+    logger.info("Service deleted")
+
+
+def test_01_09_get_all_repos_using_pub_api_case1i():
+    global repo_id1, repo_id2, service_payload1, service_payload2
+
+    logger.info("--- test_01_09_get_all_repos_using_pub_api_case1i ---")
+    service_payload1 = build_hive_service_payload(
+        name="Test_Repo_Hive_API_06_01_" + run_id,
+        description="Testing Hive Repo",
+        username="hive",
+        password="hive",
+    )
+    service_payload2 = build_hive_service_payload(
+        name="Test_Repo_Hive_API_06_02_" + run_id,
+        description="Testing Hive Repo",
+        username="hive123",
+        password="hive123",
+    )
+
+    create1 = http.post(SERVICE_URL, json=service_payload1)
+    log_http_response(logger, "POST service 1", create1)
+    create2 = http.post(SERVICE_URL, json=service_payload2)
+    log_http_response(logger, "POST service 2", create2)
+    assert_http_ok(create1, (200, 201), "POST hive service 1", SERVICE_URL)
+    assert_http_ok(create2, (200, 201), "POST hive service 2", SERVICE_URL)
+
+    repo_id1 = create1.json()["id"]
+    repo_id2 = create2.json()["id"]
+    logger.info("Created services id=%s and id=%s", repo_id1, repo_id2)
+
+    list_response = http.get(SERVICE_URL)
+    log_http_response(logger, "GET all services", list_response)
+    assert_http_ok(list_response, 200, "GET all services", SERVICE_URL)
+
+    services = list_response.json()
+    found1 = find_service_by_id(services, repo_id1)
+    found2 = find_service_by_id(services, repo_id2)
+    assert found1 is not None
+    assert found2 is not None
+    assert service_matches(service_payload1, found1)
+    assert service_matches(service_payload2, found2)
+    logger.info("Both services found in list")
+
+
+def test_01_10_get_all_policies_case1j():
+    global policy_id1, policy_id2
+
+    logger.info("--- test_01_10_get_all_policies_case1j ---")
+    policy1 = build_hive_policy_payload(
+        service_name=service_payload1["name"],
+        policy_name="test_hive_api_01_" + run_id,
+        database_list="default01,xatest01",
+        table_list="test01,test02",
+        column_list="*",
+        perm_map_list=[
+            {"groupList": ["finance"], "permList": ["select", "update"]},
+            {"userList": ["hrt_21"], "permList": ["select"]},
+            {"userList": ["hrt_22"], "permList": ["update"]},
+        ],
+    )
+    policy2 = build_hive_policy_payload(
+        service_name=service_payload2["name"],
+        policy_name="test_hive_api_02_" + run_id,
+        database_list="xatestcw,iemployee",
+        table_list="sample01,sample02",
+        column_list="*",
+        perm_map_list=[
+            {"groupList": ["audit"], "permList": ["select", "update"]},
+            {"userList": ["hrt_21"], "permList": ["update"]},
+        ],
+    )
+
+    create1 = http.post(POLICY_URL, json=policy1)
+    log_http_response(logger, "POST policy 1", create1)
+    create2 = http.post(POLICY_URL, json=policy2)
+    log_http_response(logger, "POST policy 2", create2)
+    assert_http_ok(create1, (200, 201), "POST hive policy 1", POLICY_URL)
+    assert_http_ok(create2, (200, 201), "POST hive policy 2", POLICY_URL)
+
+    policy_id1 = create1.json()["id"]
+    policy_id2 = create2.json()["id"]
+    logger.info("Created policies id=%s and id=%s", policy_id1, policy_id2)
+
+    list_response = http.get(POLICY_URL)
+    log_http_response(logger, "GET all policies", list_response)
+    assert_http_ok(list_response, 200, "GET all policies", POLICY_URL)
+
+    policies = list_response.json()
+    found1 = find_policy_by_id(policies, policy_id1)
+    found2 = find_policy_by_id(policies, policy_id2)
+    assert found1 is not None
+    assert found2 is not None
+    assert policy_matches(policy1, found1)
+    assert policy_matches(policy2, found2)
+    logger.info("Both policies found in list")
+
+
+def test_01_11_delete_all_repo_case1k():
+    global policy_id1, policy_id2, repo_id1, repo_id2
+
+    logger.info("--- test_01_11_delete_all_repo_case1k ---")
+
+    if policy_id1 is not None:
+        logger.info("DELETE policy id=%s", policy_id1)
+        http.delete(POLICY_URL + str(policy_id1))
+        policy_id1 = None
+    if policy_id2 is not None:
+        logger.info("DELETE policy id=%s", policy_id2)
+        http.delete(POLICY_URL + str(policy_id2))
+        policy_id2 = None
+
+    if repo_id1 is not None:
+        url = SERVICE_URL + str(repo_id1)
+        logger.info("DELETE service id=%s", repo_id1)
+        response = http.delete(url)
+        log_http_response(logger, "DELETE service 1", response)
+        assert_http_ok(response, (200, 204), "DELETE hive service 1", url)
+        repo_id1 = None
+
+    if repo_id2 is not None:
+        url = SERVICE_URL + str(repo_id2)
+        logger.info("DELETE service id=%s", repo_id2)
+        response = http.delete(url)
+        log_http_response(logger, "DELETE service 2", response)
+        assert_http_ok(response, (200, 204), "DELETE hive service 2", url)
+        repo_id2 = None
+
+    logger.info("Cleanup finished")
diff --git a/functional-tests/pytest.ini b/functional-tests/pytest.ini
index a815e6cfe..87e6e65d6 100644
--- a/functional-tests/pytest.ini
+++ b/functional-tests/pytest.ini
@@ -26,4 +26,12 @@ markers =
     xuserrest: xuserrest test cases
     secure_endpoint: secure endpoint test cases
     rolerest: rolerest test cases
-pythonpath = .
\ No newline at end of file
+    api: public API v2 test cases
+pythonpath = .
+# No console logging during pytest; logger.info -> logs/pytest.log (tail -f 
logs/pytest.log).
+log_cli = false
+log_file = logs/pytest.log
+log_file_mode = w
+log_file_level = INFO
+log_file_format = %(asctime)s %(levelname)s [%(name)s] %(message)s
+log_file_date_format = %Y-%m-%d %H:%M:%S
\ No newline at end of file
diff --git a/functional-tests/ranger_test_utils/utils.py 
b/functional-tests/ranger_test_utils/utils.py
new file mode 100644
index 000000000..8c1b6fd75
--- /dev/null
+++ b/functional-tests/ranger_test_utils/utils.py
@@ -0,0 +1,358 @@
+# 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.
+
+"""
+Shared helpers for Apache Ranger functional-tests (Docker pytest).
+
+"""
+
+import logging
+import uuid
+from datetime import datetime, timezone
+
+import requests
+
+# --- Ranger Admin defaults (local Docker) ---
+
+RANGER_ADMIN_BASE_URL = "http://localhost:6080/service";
+
+PUBLIC_V2_BASE = RANGER_ADMIN_BASE_URL + "/public/v2/api"
+SERVICE_URL = PUBLIC_V2_BASE + "/service/"
+POLICY_URL = PUBLIC_V2_BASE + "/policy/"
+XUSERS_BASE = RANGER_ADMIN_BASE_URL + "/xusers"
+
+DEFAULT_ADMIN_AUTH = ("admin", "rangerR0cks!")
+
+DEFAULT_JSON_HEADERS = {
+    "Accept": "application/json",
+    "Content-Type": "application/json",
+}
+
+# Users/groups used by migrated Argus public-API policy tests (created via 
xusers if missing).
+ARGUS_POLICY_USERS = ["hrt_21", "hrt_22"]
+ARGUS_POLICY_GROUPS = ["finance", "audit"]
+
+HIVE_JDBC_URL = "jdbc:hive2://127.0.0.1:10000/default"
+HIVE_DRIVER = "org.apache.hive.jdbc.HiveDriver"
+
+# --- Logging ---
+
+
+def configure_test_logging(level=logging.INFO, module_name=None):
+    """
+    Call once from setup_module.
+
+    """
+    logging.getLogger().setLevel(level)
+    if module_name:
+        logging.getLogger(module_name).setLevel(level)
+
+
+def get_test_logger(module_name):
+    """
+    Module logger for tests.
+    """
+    log = logging.getLogger(module_name)
+    log.setLevel(logging.INFO)
+    return log
+
+
+def log_http_response(logger, step, response, max_body_length=400):
+    """Log HTTP status and a short response body for each REST call."""
+    logger.info("%s: HTTP %s", step, response.status_code)
+    if not response.text:
+        return
+    body = response.text
+    if len(body) > max_body_length:
+        body = body[:max_body_length] + "..."
+    logger.info("%s response body: %s", step, body)
+
+
+# --- HTTP assertions ---
+
+
+def assert_http_ok(response, ok_codes, step, url=""):
+    """
+    Check response.status_code. On failure, raise with step, URL, and body 
text.
+    ok_codes can be 200 or (200, 201).
+    """
+    if isinstance(ok_codes, int):
+        expected = (ok_codes,)
+    else:
+        expected = ok_codes
+    if response.status_code in expected:
+        return
+    message = step + " failed: HTTP " + str(response.status_code)
+    message += " (expected " + str(expected) + ")"
+    if url:
+        message += "\nURL: " + url
+    message += "\nResponse body:\n" + response.text
+    raise AssertionError(message)
+
+
+def create_ranger_admin_session(auth=None, headers=None):
+    """
+    Build a requests.Session for Ranger Admin REST calls (Docker defaults).
+
+    Sets HTTP basic auth and JSON Accept/Content-Type headers on the session.
+    """
+    if auth is None:
+        auth = DEFAULT_ADMIN_AUTH
+    if headers is None:
+        headers = DEFAULT_JSON_HEADERS
+    session = requests.Session()
+    session.auth = auth
+    session.headers.update(headers)
+    return session
+
+
+# --- Test run IDs ---
+
+
+def unique_suffix():
+    """Unique string for service/policy names so parallel runs do not clash."""
+    time_part = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
+    random_part = uuid.uuid4().hex[:6]
+    return time_part + random_part
+
+
+# --- xusers: ensure principals exist ---
+
+
+def ensure_user_exists(session, username):
+    """Create a Ranger user via xusers REST if it is not already present."""
+    lookup_url = XUSERS_BASE + "/users/userName/" + username
+    lookup = session.get(lookup_url)
+    if lookup.status_code == 200:
+        return
+    payload = {
+        "name": username,
+        "firstName": username,
+        "lastName": "api_test",
+        "emailAddress": username + "@example.com",
+        "password": "Test@123",
+        "status": 1,
+        "isVisible": 1,
+        "userSource": 1,
+        "userRoleList": ["ROLE_USER"],
+        "groupIdList": [],
+        "groupNameList": [],
+    }
+    create_url = XUSERS_BASE + "/secure/users"
+    create = session.post(create_url, json=payload)
+    assert_http_ok(create, 200, "Create Ranger user " + username, create_url)
+
+
+def ensure_group_exists(session, group_name):
+    """Create a Ranger group via xusers REST if it is not already present."""
+    lookup_url = XUSERS_BASE + "/groups/groupName/" + group_name
+    lookup = session.get(lookup_url)
+    if lookup.status_code == 200:
+        return
+    payload = {"name": group_name, "groupSource": 0}
+    create_url = XUSERS_BASE + "/secure/groups"
+    create = session.post(create_url, json=payload)
+    assert_http_ok(create, 200, "Create Ranger group " + group_name, 
create_url)
+
+
+def ensure_argus_policy_principals(session):
+    """Create users and groups referenced by migrated Argus policy tests."""
+    for username in ARGUS_POLICY_USERS:
+        ensure_user_exists(session, username)
+    for group_name in ARGUS_POLICY_GROUPS:
+        ensure_group_exists(session, group_name)
+
+
+# --- Hive public API v2 JSON builders ---
+
+
+def split_csv(value):
+    """Turn 'a, b ,c' into ['a', 'b', 'c']."""
+    return [part.strip() for part in value.split(",") if part.strip()]
+
+
+def build_hive_service_payload(
+    name,
+    description,
+    username="policymgr",
+    password="policymgr",
+    jdbc_url=HIVE_JDBC_URL,
+    is_enabled=True,
+):
+    """JSON body for POST/PUT on /public/v2/api/service/ (Hive type)."""
+    return {
+        "name": name,
+        "type": "hive",
+        "description": description,
+        "isEnabled": is_enabled,
+        "configs": {
+            "username": username,
+            "password": password,
+            "jdbc.driverClassName": HIVE_DRIVER,
+            "jdbc.url": jdbc_url,
+            "commonNameForCertificate": "",
+        },
+    }
+
+
+def resource_block(values):
+    """Standard Ranger policy resource block (database/table/column)."""
+    return {
+        "values": values,
+        "isExcludes": False,
+        "isRecursive": False,
+    }
+
+
+def perm_map_list_to_policy_items(perm_map_list):
+    """
+    Convert legacy test shape to Public API v2 policyItems.
+
+    Each entry: {"userList": [...], "groupList": [...], "permList": ["select", 
...]}.
+    """
+    items = []
+    for entry in perm_map_list:
+        accesses = [
+            {"type": perm, "isAllowed": True}
+            for perm in entry.get("permList", [])
+        ]
+        items.append(
+            {
+                "users": entry.get("userList", []),
+                "groups": entry.get("groupList", []),
+                "accesses": accesses,
+                "delegateAdmin": False,
+            }
+        )
+    return items
+
+
+def build_hive_policy_payload(
+    service_name,
+    policy_name,
+    database_list,
+    table_list,
+    column_list,
+    perm_map_list,
+    description="",
+    is_enabled=True,
+    is_audit_enabled=True,
+    policy_id=None,
+):
+    """JSON body for POST/PUT on /public/v2/api/policy/."""
+    payload = {
+        "service": service_name,
+        "name": policy_name,
+        "description": description,
+        "isEnabled": is_enabled,
+        "isAuditEnabled": is_audit_enabled,
+        "resources": {
+            "database": resource_block(split_csv(database_list)),
+            "table": resource_block(split_csv(table_list)),
+            "column": resource_block(split_csv(column_list)),
+        },
+        "policyItems": perm_map_list_to_policy_items(perm_map_list),
+    }
+    if policy_id is not None:
+        payload["id"] = policy_id
+    return payload
+
+
+# --- Compare expected vs actual service/policy JSON ---
+
+
+def service_matches(expected, actual, attributes=None):
+    """True if selected top-level service fields match."""
+    if attributes is None:
+        attributes = ["name", "description", "type"]
+    return all(actual.get(key) == expected.get(key) for key in attributes)
+
+
+def get_allowed_access_types(policy_item):
+    """Sorted list of permission types marked isAllowed on a policy item."""
+    types = [
+        access.get("type")
+        for access in policy_item.get("accesses", [])
+        if access.get("isAllowed", True)
+    ]
+    return sorted(types)
+
+
+def single_policy_item_matches(expected_item, actual_item):
+    """True if users, groups, and allowed access types match one policy 
item."""
+    if sorted(expected_item.get("users", [])) != 
sorted(actual_item.get("users", [])):
+        return False
+    if sorted(expected_item.get("groups", [])) != sorted(
+        actual_item.get("groups", [])
+    ):
+        return False
+    return get_allowed_access_types(expected_item) == get_allowed_access_types(
+        actual_item
+    )
+
+
+def policy_items_match(expected_items, actual_items):
+    """True if every expected policy item has a matching actual item (order 
ignored)."""
+    if len(expected_items) != len(actual_items):
+        return False
+    for expected_item in expected_items:
+        if not any(
+            single_policy_item_matches(expected_item, actual_item)
+            for actual_item in actual_items
+        ):
+            return False
+    return True
+
+
+def resource_values_match(expected_resources, actual_resources, resource_name):
+    """True if database/table/column value lists match (order ignored)."""
+    exp_vals = sorted(
+        expected_resources.get(resource_name, {}).get("values", [])
+    )
+    act_vals = sorted(actual_resources.get(resource_name, {}).get("values", 
[]))
+    return exp_vals == act_vals
+
+
+def policy_matches(expected, actual):
+    """True if name, service, hive resources, and policy items match."""
+    if expected.get("name") != actual.get("name"):
+        return False
+    if expected.get("service") != actual.get("service"):
+        return False
+    exp_res = expected.get("resources", {})
+    act_res = actual.get("resources", {})
+    for resource_name in ("database", "table", "column"):
+        if not resource_values_match(exp_res, act_res, resource_name):
+            return False
+    return policy_items_match(
+        expected.get("policyItems", []),
+        actual.get("policyItems", []),
+    )
+
+
+def find_service_by_id(services, service_id):
+    """Return the service dict with the given id, or None."""
+    for service in services:
+        if service.get("id") == service_id:
+            return service
+    return None
+
+
+def find_policy_by_id(policies, policy_id):
+    """Return the policy dict with the given id, or None."""
+    for policy in policies:
+        if policy.get("id") == policy_id:
+            return policy
+    return None
diff --git a/functional-tests/readme.md b/functional-tests/readme.md
index 4fd0ec485..2104d57f9 100644
--- a/functional-tests/readme.md
+++ b/functional-tests/readme.md
@@ -30,6 +30,7 @@ This test suite validates REST API endpoints for Apache 
Ranger services — Admi
 | **rolerest** | Test cases for Ranger Admin Role REST APIs |
 | **servicerest** | Test cases for Ranger Admin Service REST APIs |
 | **tagrest** | Test cases for Ranger Admin Tag REST APIs |
+| **api** | Public API v2 service and policy CRUD (admin Docker only) |
 
 ---
 
@@ -37,13 +38,15 @@ This test suite validates REST API endpoints for Apache 
Ranger services — Admi
 
 ```text
 functional-tests/
+├── ranger_test_utils/utils.py   # Shared logging, HTTP asserts, Public API v2 
helpers
 ├── hdfs/                        # Tests on HDFS encryption cycle
 ├── kms/                         # Tests on KMS REST API
 ├── xuserrest/                   # Tests on Ranger User/Group/Role REST APIs
 ├── rolerest/                    # Tests on Ranger Role REST APIs
 ├── servicerest/                 # Tests on Ranger Service REST APIs
 ├── tagrest/                     # Tests on Ranger Tag REST APIs
-│ 
+├── api/                         # Public API v2 (service / policy) tests
+│
 ├── pytest.ini                   # Registers custom pytest markers
 ├── run-tests.sh                 # Script to automate setup and test execution
 ├── requirements.txt             # Python dependencies
@@ -120,9 +123,10 @@ export AUDIT_INDEX_STORE=none
 ---
 
 ## Running Tests
-
 The `run-tests.sh` script manages Docker container setup, dependency 
installation, and test execution. It supports both interactive and 
argument-based modes.
 
+1. Interactive Mode:
+
 ### 1. Interactive Mode
 
 Run the script without arguments to be prompted for each input:
@@ -206,5 +210,4 @@ Regardless of which test suites you choose, these 
containers always start:
 ---
 
 ## Test Reports
-
 After execution, an HTML report is automatically generated for each suite in 
the `functional-tests/` directory:
diff --git a/functional-tests/run-tests.sh b/functional-tests/run-tests.sh
index 758681911..8b8f2ebbd 100755
--- a/functional-tests/run-tests.sh
+++ b/functional-tests/run-tests.sh
@@ -16,13 +16,14 @@
 #!/bin/bash
 
 # All available test suites (pytest folders)
-ALL_TEST_SUITES=(rolerest xuserrest servicerest tagrest hdfs kms)
+
+ALL_TEST_SUITES=(rolerest xuserrest servicerest tagrest hdfs kms api)
 
 # Suites that have actual docker-compose services
 DOCKER_SERVICES=(hdfs kms)
 
 # Test-only suites (no docker-compose file needed)
-TEST_ONLY_SUITES=(rolerest xuserrest servicerest tagrest)
+TEST_ONLY_SUITES=(rolerest xuserrest servicerest tagrest api)
 
 #handle input
 DB_TYPE="${1:-}"

Reply via email to