Skip to content

Commit 3be7e83

Browse files
committed
refactor(dpi_ng): enforce abstract service_path on BaseCapabilityConfig
1 parent b17e4ac commit 3be7e83

3 files changed

Lines changed: 41 additions & 21 deletions

File tree

src/sap_cloud_sdk/core/dpi_ng/config.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
import re
5+
from abc import ABC, abstractmethod
56
from dataclasses import dataclass, field
67

78
from .auth import AuthProvider, ClientCertificateAuth
@@ -12,11 +13,11 @@
1213

1314

1415
@dataclass
15-
class BaseCapabilityConfig:
16+
class BaseCapabilityConfig(ABC):
1617
"""Base configuration for a DPI NG capability client.
1718
18-
Subclasses add a ``service_path`` (and any other capability-specific fields)
19-
on top of these shared fields.
19+
Subclasses override ``service_path`` with a capability-specific default
20+
and may add extra fields on top of these shared fields.
2021
2122
Args:
2223
base_url: URL of the DPI external service router
@@ -25,6 +26,8 @@ class BaseCapabilityConfig:
2526
service instance.
2627
auth: Authentication strategy - one of BearerTokenAuth, ClientCredentialsAuth,
2728
or ClientCertificateAuth.
29+
service_path: Base OData path used to build service URLs. Subclasses
30+
override this field with a capability-specific default.
2831
timeout: HTTP request timeout in seconds (default 30).
2932
verify_ssl: Verify TLS certificates - set False only in local dev.
3033
Overridden by ``ClientCertificateAuth`` when a custom ``ca_file`` is provided.
@@ -42,6 +45,11 @@ class BaseCapabilityConfig:
4245
verify_ssl: bool = True
4346
tenant_id: str | None = field(default=None)
4447

48+
@property
49+
@abstractmethod
50+
def service_path(self) -> str:
51+
"""Base OData path used to build service URLs."""
52+
4553
def __post_init__(self) -> None:
4654
"""Validate config after dataclass construction.
4755

tests/core/unit/dpi_ng/unit/test_base_config.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Unit tests for BaseCapabilityConfig — shared fields and tenant_id validation."""
22

33
import pytest
4+
from dataclasses import dataclass
45
from unittest.mock import MagicMock
56

67
from sap_cloud_sdk.core.dpi_ng.auth import AuthProvider, ClientCertificateAuth
@@ -11,89 +12,94 @@ def valid_auth():
1112
return MagicMock(spec=AuthProvider)
1213

1314

15+
@dataclass
16+
class _TestConfig(BaseCapabilityConfig):
17+
service_path: str = "/sap/test/odata/v4"
18+
19+
1420
class TestValidConstruction:
1521
def test_https_url_accepted(self):
16-
cfg = BaseCapabilityConfig(base_url="https://example.com", auth=valid_auth())
22+
cfg = _TestConfig(base_url="https://example.com", auth=valid_auth())
1723
assert cfg.base_url == "https://example.com"
1824

1925
def test_http_url_accepted(self):
20-
cfg = BaseCapabilityConfig(base_url="http://example.com", auth=valid_auth())
26+
cfg = _TestConfig(base_url="http://example.com", auth=valid_auth())
2127
assert cfg.base_url == "http://example.com"
2228

2329
def test_trailing_slash_stripped(self):
24-
cfg = BaseCapabilityConfig(base_url="https://example.com/", auth=valid_auth())
30+
cfg = _TestConfig(base_url="https://example.com/", auth=valid_auth())
2531
assert cfg.base_url == "https://example.com"
2632

2733
def test_multiple_trailing_slashes_stripped(self):
28-
cfg = BaseCapabilityConfig(base_url="https://example.com///", auth=valid_auth())
34+
cfg = _TestConfig(base_url="https://example.com///", auth=valid_auth())
2935
assert cfg.base_url == "https://example.com"
3036

3137
def test_auth_stored(self):
3238
auth = valid_auth()
33-
cfg = BaseCapabilityConfig(base_url="https://example.com", auth=auth)
39+
cfg = _TestConfig(base_url="https://example.com", auth=auth)
3440
assert cfg.auth is auth
3541

3642

3743
class TestDefaults:
3844
def test_timeout_default(self):
39-
cfg = BaseCapabilityConfig(base_url="https://example.com", auth=valid_auth())
45+
cfg = _TestConfig(base_url="https://example.com", auth=valid_auth())
4046
assert cfg.timeout == 30.0
4147

4248
def test_verify_ssl_default(self):
43-
cfg = BaseCapabilityConfig(base_url="https://example.com", auth=valid_auth())
49+
cfg = _TestConfig(base_url="https://example.com", auth=valid_auth())
4450
assert cfg.verify_ssl is True
4551

4652
def test_tenant_id_default_is_none(self):
47-
cfg = BaseCapabilityConfig(base_url="https://example.com", auth=valid_auth())
53+
cfg = _TestConfig(base_url="https://example.com", auth=valid_auth())
4854
assert cfg.tenant_id is None
4955

5056

5157
class TestInvalidBaseUrl:
5258
def test_empty_string_raises(self):
5359
with pytest.raises(ValueError, match="base_url must be a valid HTTP"):
54-
BaseCapabilityConfig(base_url="", auth=valid_auth())
60+
_TestConfig(base_url="", auth=valid_auth())
5561

5662
def test_plain_string_raises(self):
5763
with pytest.raises(ValueError, match="base_url must be a valid HTTP"):
58-
BaseCapabilityConfig(base_url="not-a-url", auth=valid_auth())
64+
_TestConfig(base_url="not-a-url", auth=valid_auth())
5965

6066
def test_ftp_scheme_raises(self):
6167
with pytest.raises(ValueError, match="base_url must be a valid HTTP"):
62-
BaseCapabilityConfig(base_url="ftp://example.com", auth=valid_auth())
68+
_TestConfig(base_url="ftp://example.com", auth=valid_auth())
6369

6470
def test_missing_scheme_raises(self):
6571
with pytest.raises(ValueError, match="base_url must be a valid HTTP"):
66-
BaseCapabilityConfig(base_url="example.com", auth=valid_auth())
72+
_TestConfig(base_url="example.com", auth=valid_auth())
6773

6874

6975
class TestInvalidAuth:
7076
def test_none_auth_raises(self):
7177
with pytest.raises(ValueError, match="auth must be an AuthProvider"):
72-
BaseCapabilityConfig(base_url="https://example.com", auth=None) # ty: ignore[invalid-argument-type]
78+
_TestConfig(base_url="https://example.com", auth=None) # ty: ignore[invalid-argument-type]
7379

7480
def test_string_auth_raises(self):
7581
with pytest.raises(ValueError, match="auth must be an AuthProvider"):
76-
BaseCapabilityConfig(base_url="https://example.com", auth="Bearer token") # ty: ignore[invalid-argument-type]
82+
_TestConfig(base_url="https://example.com", auth="Bearer token") # ty: ignore[invalid-argument-type]
7783

7884

7985
class TestTenantId:
8086
def test_cert_auth_without_tenant_id_raises(self):
8187
with pytest.raises(ValueError, match="tenant_id is required"):
82-
BaseCapabilityConfig(
88+
_TestConfig(
8389
base_url="https://example.com",
8490
auth=ClientCertificateAuth(cert_file="cert.pem", key_file="key.pem"),
8591
)
8692

8793
def test_non_cert_auth_with_tenant_id_raises(self):
8894
with pytest.raises(ValueError, match="tenant_id must not be set"):
89-
BaseCapabilityConfig(
95+
_TestConfig(
9096
base_url="https://example.com",
9197
auth=valid_auth(),
9298
tenant_id="tenant-123",
9399
)
94100

95101
def test_cert_auth_with_tenant_id_stored(self):
96-
cfg = BaseCapabilityConfig(
102+
cfg = _TestConfig(
97103
base_url="https://example.com",
98104
auth=ClientCertificateAuth(cert_file="cert.pem", key_file="key.pem"),
99105
tenant_id="tenant-abc",

tests/core/unit/dpi_ng/unit/test_base_odata_client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from dataclasses import dataclass
56
from unittest.mock import MagicMock, patch
67

78
import pytest
@@ -11,9 +12,14 @@
1112
from sap_cloud_sdk.core.dpi_ng.odata_client import BaseODataClient
1213

1314

15+
@dataclass
16+
class _TestConfig(BaseCapabilityConfig):
17+
service_path: str = "/test/odata/v4"
18+
19+
1420
def _make_config():
1521
auth = MagicMock(spec=AuthProvider)
16-
return BaseCapabilityConfig(base_url="https://example.com", auth=auth)
22+
return _TestConfig(base_url="https://example.com", auth=auth)
1723

1824

1925
class _ConcreteClient(BaseODataClient):

0 commit comments

Comments
 (0)