297 lines
10 KiB
Python
297 lines
10 KiB
Python
|
|
# Copyright (C) CVAT.ai Corporation
|
||
|
|
#
|
||
|
|
# SPDX-License-Identifier: MIT
|
||
|
|
|
||
|
|
import io
|
||
|
|
from contextlib import ExitStack
|
||
|
|
from logging import Logger
|
||
|
|
|
||
|
|
import packaging.version as pv
|
||
|
|
import pytest
|
||
|
|
from cvat_sdk import Client, models
|
||
|
|
from cvat_sdk.core.client import Config, make_client
|
||
|
|
from cvat_sdk.core.exceptions import IncompatibleVersionException, InvalidHostException
|
||
|
|
from cvat_sdk.exceptions import ApiException
|
||
|
|
|
||
|
|
from shared.utils.config import BASE_URL, USER_PASS
|
||
|
|
|
||
|
|
|
||
|
|
class TestClientUsecases:
|
||
|
|
@pytest.fixture(autouse=True)
|
||
|
|
def setup(
|
||
|
|
self,
|
||
|
|
restore_db_per_function, # force fixture call order to allow DB setup
|
||
|
|
fxt_logger: tuple[Logger, io.StringIO],
|
||
|
|
fxt_client: Client,
|
||
|
|
fxt_stdout: io.StringIO,
|
||
|
|
admin_user: str,
|
||
|
|
):
|
||
|
|
_, self.logger_stream = fxt_logger
|
||
|
|
self.client = fxt_client
|
||
|
|
self.stdout = fxt_stdout
|
||
|
|
self.user = admin_user
|
||
|
|
|
||
|
|
yield
|
||
|
|
|
||
|
|
def test_can_login_with_basic_auth(self):
|
||
|
|
self.client.login((self.user, USER_PASS))
|
||
|
|
|
||
|
|
assert self.client.has_credentials()
|
||
|
|
|
||
|
|
def test_can_fail_to_login_with_basic_auth(self):
|
||
|
|
with pytest.raises(ApiException):
|
||
|
|
self.client.login((self.user, USER_PASS + "123"))
|
||
|
|
|
||
|
|
def test_can_logout(self):
|
||
|
|
self.client.login((self.user, USER_PASS))
|
||
|
|
|
||
|
|
self.client.logout()
|
||
|
|
|
||
|
|
assert not self.client.has_credentials()
|
||
|
|
|
||
|
|
def test_can_get_server_version(self):
|
||
|
|
self.client.login((self.user, USER_PASS))
|
||
|
|
|
||
|
|
version = self.client.get_server_version()
|
||
|
|
|
||
|
|
assert (version.major, version.minor) >= (2, 0)
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_strip_trailing_slash_in_hostname_in_make_client(admin_user: str):
|
||
|
|
host, port = BASE_URL.split("://", maxsplit=1)[1].rsplit(":", maxsplit=1)
|
||
|
|
|
||
|
|
with make_client(host=host + "/", port=port, credentials=(admin_user, USER_PASS)) as client:
|
||
|
|
assert client.api_map.host == BASE_URL
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_strip_trailing_slash_in_hostname_in_client_ctor(admin_user: str):
|
||
|
|
with Client(url=BASE_URL + "/") as client:
|
||
|
|
client.login((admin_user, USER_PASS))
|
||
|
|
assert client.api_map.host == BASE_URL
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_detect_server_schema_if_not_provided():
|
||
|
|
host, port = BASE_URL.split("://", maxsplit=1)[1].rsplit(":", maxsplit=1)
|
||
|
|
client = make_client(host=host, port=int(port))
|
||
|
|
assert client.api_map.host == "http://" + host + ":" + port
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_fail_to_detect_server_schema_if_not_provided():
|
||
|
|
host, port = BASE_URL.split("://", maxsplit=1)[1].rsplit(":", maxsplit=1)
|
||
|
|
with pytest.raises(InvalidHostException) as capture:
|
||
|
|
make_client(host=host, port=int(port) + 1)
|
||
|
|
|
||
|
|
assert capture.match(r"Failed to detect host schema automatically")
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_reject_invalid_server_schema():
|
||
|
|
host, port = BASE_URL.split("://", maxsplit=1)[1].rsplit(":", maxsplit=1)
|
||
|
|
with pytest.raises(InvalidHostException) as capture:
|
||
|
|
make_client(host="ftp://" + host, port=int(port) + 1)
|
||
|
|
|
||
|
|
assert capture.match(r"Invalid url schema 'ftp'")
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("raise_exception", (True, False))
|
||
|
|
def test_can_warn_on_mismatching_server_version(
|
||
|
|
fxt_logger: tuple[Logger, io.StringIO], monkeypatch, raise_exception: bool
|
||
|
|
):
|
||
|
|
logger, logger_stream = fxt_logger
|
||
|
|
|
||
|
|
def mocked_version(_):
|
||
|
|
return pv.Version("0")
|
||
|
|
|
||
|
|
monkeypatch.setattr(Client, "get_server_version", mocked_version)
|
||
|
|
|
||
|
|
config = Config()
|
||
|
|
|
||
|
|
with ExitStack() as es:
|
||
|
|
if raise_exception:
|
||
|
|
config.allow_unsupported_server = False
|
||
|
|
es.enter_context(pytest.raises(IncompatibleVersionException))
|
||
|
|
|
||
|
|
Client(url=BASE_URL, logger=logger, config=config)
|
||
|
|
|
||
|
|
assert "Server version '0' is not compatible with SDK version" in logger_stream.getvalue()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("do_check", (True, False))
|
||
|
|
def test_can_check_server_version_in_ctor(
|
||
|
|
fxt_logger: tuple[Logger, io.StringIO], monkeypatch, do_check: bool
|
||
|
|
):
|
||
|
|
logger, logger_stream = fxt_logger
|
||
|
|
|
||
|
|
def mocked_version(_):
|
||
|
|
return pv.Version("0")
|
||
|
|
|
||
|
|
monkeypatch.setattr(Client, "get_server_version", mocked_version)
|
||
|
|
|
||
|
|
config = Config()
|
||
|
|
config.allow_unsupported_server = False
|
||
|
|
|
||
|
|
with ExitStack() as es:
|
||
|
|
if do_check:
|
||
|
|
es.enter_context(pytest.raises(IncompatibleVersionException))
|
||
|
|
|
||
|
|
Client(url=BASE_URL, logger=logger, config=config, check_server_version=do_check)
|
||
|
|
|
||
|
|
assert (
|
||
|
|
"Server version '0' is not compatible with SDK version" in logger_stream.getvalue()
|
||
|
|
) == do_check
|
||
|
|
|
||
|
|
|
||
|
|
def test_can_check_server_version_in_method(fxt_logger: tuple[Logger, io.StringIO], monkeypatch):
|
||
|
|
logger, logger_stream = fxt_logger
|
||
|
|
|
||
|
|
def mocked_version(_):
|
||
|
|
return pv.Version("0")
|
||
|
|
|
||
|
|
monkeypatch.setattr(Client, "get_server_version", mocked_version)
|
||
|
|
|
||
|
|
config = Config()
|
||
|
|
config.allow_unsupported_server = False
|
||
|
|
client = Client(url=BASE_URL, logger=logger, config=config, check_server_version=False)
|
||
|
|
|
||
|
|
with client, pytest.raises(IncompatibleVersionException):
|
||
|
|
client.check_server_version()
|
||
|
|
|
||
|
|
assert "Server version '0' is not compatible with SDK version" in logger_stream.getvalue()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"server_version, supported_versions, expect_supported",
|
||
|
|
[
|
||
|
|
# Currently, it is ~=, as defined in https://peps.python.org/pep-0440/
|
||
|
|
("3.2", ["2.0"], False),
|
||
|
|
("2", ["2.1"], False),
|
||
|
|
("2.1", ["2.1"], True),
|
||
|
|
("2.1a", ["2.1"], False),
|
||
|
|
("2.1.post1", ["2.1"], True),
|
||
|
|
("2.1", ["2.1.pre1"], True),
|
||
|
|
("2.1.1", ["2.1"], True),
|
||
|
|
("2.2", ["2.1"], False),
|
||
|
|
("2.2", ["2.1.0", "2.3"], False),
|
||
|
|
("2.2", ["2.1", "2.2", "2.3"], True),
|
||
|
|
("2.2.post1", ["2.1", "2.2", "2.3"], True),
|
||
|
|
("2.2.pre1", ["2.1", "2.2", "2.3"], False),
|
||
|
|
("2.2", ["2.3"], False),
|
||
|
|
("2.1.0.dev123", ["2.1.post2"], False),
|
||
|
|
("1!1.3", ["2.1"], False),
|
||
|
|
("1!1.3.1", ["2.1", "1!1.3"], True),
|
||
|
|
("1!1.1.dev12", ["1!1.1"], False),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_can_check_server_version_compatibility(
|
||
|
|
fxt_logger: tuple[Logger, io.StringIO],
|
||
|
|
monkeypatch: pytest.MonkeyPatch,
|
||
|
|
server_version: str,
|
||
|
|
supported_versions: list[str],
|
||
|
|
expect_supported: bool,
|
||
|
|
):
|
||
|
|
logger, _ = fxt_logger
|
||
|
|
|
||
|
|
monkeypatch.setattr(Client, "get_server_version", lambda _: pv.Version(server_version))
|
||
|
|
monkeypatch.setattr(
|
||
|
|
Client, "SUPPORTED_SERVER_VERSIONS", [pv.Version(v) for v in supported_versions]
|
||
|
|
)
|
||
|
|
config = Config(allow_unsupported_server=False)
|
||
|
|
|
||
|
|
with ExitStack() as es:
|
||
|
|
if not expect_supported:
|
||
|
|
es.enter_context(pytest.raises(IncompatibleVersionException))
|
||
|
|
|
||
|
|
Client(url=BASE_URL, logger=logger, config=config, check_server_version=True)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("verify", [True, False])
|
||
|
|
def test_can_control_ssl_verification_with_config(verify: bool):
|
||
|
|
config = Config(verify_ssl=verify)
|
||
|
|
|
||
|
|
client = Client(BASE_URL, config=config)
|
||
|
|
|
||
|
|
assert client.api_client.configuration.verify_ssl == verify
|
||
|
|
|
||
|
|
|
||
|
|
def test_organization_contexts(admin_user: str):
|
||
|
|
with make_client(BASE_URL, credentials=(admin_user, USER_PASS)) as client:
|
||
|
|
assert client.organization_slug is None
|
||
|
|
|
||
|
|
org = client.organizations.create(models.OrganizationWriteRequest(slug="testorg"))
|
||
|
|
|
||
|
|
# create a project in the personal workspace
|
||
|
|
client.organization_slug = ""
|
||
|
|
personal_project = client.projects.create(models.ProjectWriteRequest(name="Personal"))
|
||
|
|
assert personal_project.organization is None
|
||
|
|
|
||
|
|
# create a project in the organization
|
||
|
|
client.organization_slug = org.slug
|
||
|
|
org_project = client.projects.create(models.ProjectWriteRequest(name="Org"))
|
||
|
|
assert org_project.organization == org.id
|
||
|
|
|
||
|
|
# both projects should be visible with no context
|
||
|
|
client.organization_slug = None
|
||
|
|
client.projects.retrieve(personal_project.id)
|
||
|
|
client.projects.retrieve(org_project.id)
|
||
|
|
|
||
|
|
# retrieve personal and org projects by id
|
||
|
|
client.organization_slug = ""
|
||
|
|
client.projects.retrieve(personal_project.id)
|
||
|
|
client.projects.retrieve(org_project.id)
|
||
|
|
|
||
|
|
# org context doesn't make sense for detailed request
|
||
|
|
client.organization_slug = org.slug
|
||
|
|
client.projects.retrieve(org_project.id)
|
||
|
|
client.projects.retrieve(personal_project.id)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.usefixtures("restore_db_per_function")
|
||
|
|
def test_organization_filtering(regular_lonely_user: str, fxt_image_file):
|
||
|
|
with make_client(BASE_URL, credentials=(regular_lonely_user, USER_PASS)) as client:
|
||
|
|
org = client.organizations.create(models.OrganizationWriteRequest(slug="testorg"))
|
||
|
|
|
||
|
|
# create a project and task in sandbox
|
||
|
|
client.organization_slug = None
|
||
|
|
client.projects.create(models.ProjectWriteRequest(name="personal_project"))
|
||
|
|
client.tasks.create_from_data(
|
||
|
|
spec={"name": "personal_task", "labels": [{"name": "a"}]}, resources=[fxt_image_file]
|
||
|
|
)
|
||
|
|
|
||
|
|
# create a project and task in the organization
|
||
|
|
client.organization_slug = org.slug
|
||
|
|
client.projects.create(models.ProjectWriteRequest(name="org_project"))
|
||
|
|
client.tasks.create_from_data(
|
||
|
|
spec={"name": "org_task", "labels": [{"name": "a"}]}, resources=[fxt_image_file]
|
||
|
|
)
|
||
|
|
|
||
|
|
# return only non-org objects if org parameter is empty
|
||
|
|
client.organization_slug = ""
|
||
|
|
projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list()
|
||
|
|
|
||
|
|
assert len(projects) == len(tasks) == len(jobs) == 1
|
||
|
|
assert projects[0].organization == tasks[0].organization == jobs[0].organization == None
|
||
|
|
|
||
|
|
# return all objects if org parameter wasn't presented
|
||
|
|
client.organization_slug = None
|
||
|
|
projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list()
|
||
|
|
|
||
|
|
assert len(projects) == len(tasks) == len(jobs) == 2
|
||
|
|
assert {None, org.id} == set([a.organization for a in (*projects, *tasks, *jobs)])
|
||
|
|
|
||
|
|
# return only org objects if org parameter is presented and not empty
|
||
|
|
client.organization_slug = org.slug
|
||
|
|
projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list()
|
||
|
|
|
||
|
|
assert len(projects) == len(tasks) == len(jobs) == 1
|
||
|
|
assert projects[0].organization == tasks[0].organization == jobs[0].organization == org.id
|
||
|
|
|
||
|
|
|
||
|
|
def test_organization_context_manager():
|
||
|
|
client = Client(BASE_URL)
|
||
|
|
|
||
|
|
client.organization_slug = "abc"
|
||
|
|
|
||
|
|
with client.organization_context("def"):
|
||
|
|
assert client.organization_slug == "def"
|
||
|
|
|
||
|
|
assert client.organization_slug == "abc"
|