Skip to content

Commit f535801

Browse files
authored
DEVEXP-729: E2E Tests - Available Numbers (#45)
1 parent 04945ed commit f535801

24 files changed

+402
-74
lines changed
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/bash
2+
3+
wait_for_server() {
4+
local url=$1
5+
echo "Waiting for $url to be ready..."
6+
7+
MAX_RETRIES="${MAX_RETRIES:-30}"
8+
SLEEP_SECONDS="${SLEEP_SECONDS:-2}"
9+
10+
for ((i = 1; i <= MAX_RETRIES; i++)); do
11+
if curl -sSf "$url" > /dev/null; then
12+
echo "$url is ready!"
13+
return 0
14+
fi
15+
echo "Attempt $i/$MAX_RETRIES: Still waiting for $url..."
16+
sleep "$SLEEP_SECONDS"
17+
done
18+
19+
echo "Error: $url was not available after $((MAX_RETRIES * SLEEP_SECONDS)) seconds"
20+
exit 1
21+
}
22+
23+
# Wait for auth mock servers
24+
wait_for_server "http://localhost:3011/health"
25+
# Wait for numbers mock servers
26+
wait_for_server "http://localhost:3013/health"
27+
28+
echo "All mock servers are ready!"

.github/workflows/run-tests.yml

+30
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,33 @@ jobs:
4545
- name: Coverage Test Report
4646
run: |
4747
python -m coverage report --skip-empty
48+
49+
- name: Checkout sinch-sdk-mockserver repository
50+
uses: actions/checkout@v3
51+
with:
52+
repository: sinch/sinch-sdk-mockserver
53+
token: ${{ secrets.PAT_CI }}
54+
fetch-depth: 0
55+
path: sinch-sdk-mockserver
56+
57+
- name: Install Docker Compose
58+
run: |
59+
sudo apt-get update
60+
sudo apt-get install -y docker-compose
61+
62+
- name: Start mock servers with Docker Compose
63+
run: |
64+
cd sinch-sdk-mockserver
65+
docker-compose up -d
66+
67+
- name: Copy feature files
68+
run: |
69+
cp sinch-sdk-mockserver/features/numbers/numbers.feature ./tests/e2e/numbers/features/
70+
71+
- name: Wait for mock server
72+
run: .github/scripts/wait-for-mockserver.sh
73+
shell: bash
74+
75+
- name: Run e2e tests
76+
run: behave tests/e2e/**/features
77+

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ coverage.xml
5252
.pytest_cache/
5353
cover/
5454

55+
# E2E features
56+
*.feature
57+
5558
# Translations
5659
*.mo
5760
*.pot

requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pytest
33
pytest-asyncio
44
pytest-mock
55
coverage
6+
behave
67

78
# Code Quality
89
flake8

sinch/core/clients/sinch_client_configuration.py

-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def __init__(
2020
token_manager: Union[TokenManager, TokenManagerAsync],
2121
logger: Logger = None,
2222
logger_name: str = None,
23-
disable_https=False,
2423
connection_timeout=10,
2524
application_key: str = None,
2625
application_secret: str = None,
@@ -51,7 +50,6 @@ def __init__(
5150
self._templates_region = "eu"
5251
self._templates_domain = ".template.api.sinch.com"
5352
self.token_manager = token_manager
54-
self.disable_https = disable_https
5553
self.transport: HTTPTransport = transport
5654

5755
self._set_conversation_origin()

sinch/core/endpoint.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,34 @@
44

55
class HTTPEndpoint(ABC):
66
ENDPOINT_URL = None
7-
HTTP_METHOD = None
8-
HTTP_AUTHENTICATION = None
97

10-
def __init__(self, project_id, request_data):
8+
@property
9+
@abstractmethod
10+
def HTTP_METHOD(self) -> str:
11+
pass
12+
13+
@property
14+
@abstractmethod
15+
def HTTP_AUTHENTICATION(self) -> str:
1116
pass
1217

18+
def __init__(self, project_id, request_data):
19+
self.project_id = project_id
20+
self.request_data = request_data
21+
1322
def get_url_without_origin(self, sinch):
1423
return '/' + '/'.join(self.build_url(sinch).split('/')[1:])
1524

1625
def build_url(self, sinch):
1726
return
1827

1928
def build_query_params(self):
29+
"""
30+
Constructs the query parameters for the endpoint.
31+
32+
Returns:
33+
dict: The query parameters to be sent with the API request.
34+
"""
2035
pass
2136

2237
def request_body(self):

sinch/core/models/http_request.py

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
@dataclass
55
class HttpRequest:
66
headers: dict
7-
protocol: str
87
url: str
98
http_method: str
109
request_body: dict

sinch/core/ports/http_transport.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,14 @@ def authenticate(self, endpoint, request_data):
8181
return request_data
8282

8383
def prepare_request(self, endpoint: HTTPEndpoint) -> HttpRequest:
84-
protocol = "http://" if self.sinch.configuration.disable_https else "https://"
8584
url_query_params = endpoint.build_query_params()
8685

8786
return HttpRequest(
8887
headers={
8988
"User-Agent": f"sinch-sdk/{sdk_version} (Python/{python_version()};"
9089
f" {self.__class__.__name__};)"
9190
},
92-
protocol=protocol,
93-
url=protocol + endpoint.build_url(self.sinch),
91+
url=endpoint.build_url(self.sinch),
9492
http_method=endpoint.HTTP_METHOD,
9593
request_body=endpoint.request_body(),
9694
query_params=url_query_params,

sinch/domains/numbers/available_numbers.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
from typing import Optional, TypedDict, overload, Literal, Union, Annotated
22
from typing_extensions import NotRequired
33
from pydantic import StrictInt, StrictStr, Field
4-
from sinch.domains.numbers.endpoints.available.search_for_number import SearchForNumberEndpoint
5-
from sinch.domains.numbers.endpoints.available.list_available_numbers import AvailableNumbersEndpoint
6-
from sinch.domains.numbers.endpoints.available.activate_number import ActivateNumberEndpoint
7-
from sinch.domains.numbers.endpoints.available.rent_any_number import RentAnyNumberEndpoint
4+
from sinch.domains.numbers.endpoints.available.search_for_number_endpoint import SearchForNumberEndpoint
5+
from sinch.domains.numbers.endpoints.available.list_available_numbers_endpoint import AvailableNumbersEndpoint
6+
from sinch.domains.numbers.endpoints.available.activate_number_endpoint import ActivateNumberEndpoint
7+
from sinch.domains.numbers.endpoints.available.rent_any_number_endpoint import RentAnyNumberEndpoint
88

99
from sinch.domains.numbers.models.available.list_available_numbers_request import ListAvailableNumbersRequest
1010
from sinch.domains.numbers.models.available.activate_number_request import ActivateNumberRequest
1111
from sinch.domains.numbers.models.available.check_number_availability_request import CheckNumberAvailabilityRequest
1212
from sinch.domains.numbers.models.available.rent_any_number_request import RentAnyNumberRequest
1313

14-
from sinch.domains.numbers.models.available.list_available_numbers_response import ListAvailableNumbersResponse
1514
from sinch.domains.numbers.models.available.activate_number_response import ActivateNumberResponse
1615
from sinch.domains.numbers.models.available.check_number_availability_response import CheckNumberAvailabilityResponse
1716
from sinch.domains.numbers.models.available.rent_any_number_response import RentAnyNumberResponse
17+
from sinch.domains.numbers.models.numbers import Number
1818

1919
from sinch.domains.numbers.models.numbers import NumberTypeValues, CapabilityTypeValues, NumberSearchPatternTypeValues
2020

@@ -86,7 +86,7 @@ def list(
8686
capabilities: Optional[CapabilityTypeValues] = None,
8787
page_size: Optional[StrictInt] = None,
8888
**kwargs
89-
) -> ListAvailableNumbersResponse:
89+
) -> list[Number]:
9090
"""
9191
Search for available virtual numbers for you to activate using a variety of parameters to filter results.
9292
@@ -101,7 +101,7 @@ def list(
101101
**kwargs: Additional filters for the request.
102102
103103
Returns:
104-
ListAvailableNumbersResponse: A response object with available numbers and their details.
104+
list[Number]: A response array with available numbers and their details.
105105
106106
For detailed documentation, visit https://developers.sinch.com
107107
"""
@@ -121,8 +121,8 @@ def list(
121121
def activate(
122122
self,
123123
phone_number: StrictStr,
124-
sms_configuration: None,
125-
voice_configuration: None,
124+
sms_configuration: Optional[SmsConfigurationDict] = None,
125+
voice_configuration: Optional[VoiceConfigurationDictType] = None,
126126
callback_url: Optional[StrictStr] = None
127127
) -> ActivateNumberResponse:
128128
pass

sinch/domains/numbers/endpoints/available/activate_number.py sinch/domains/numbers/endpoints/available/activate_number_endpoint.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from sinch.core.enums import HTTPAuthentication, HTTPMethods
22
from sinch.core.models.http_response import HTTPResponse
33
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
4+
from sinch.domains.numbers.exceptions import NumberNotFoundException, NumbersException
45
from sinch.domains.numbers.models.available.activate_number_request import ActivateNumberRequest
56
from sinch.domains.numbers.models.available.activate_number_response import ActivateNumberResponse
67

@@ -17,5 +18,9 @@ def __init__(self, project_id: str, request_data: ActivateNumberRequest):
1718
super(ActivateNumberEndpoint, self).__init__(project_id, request_data)
1819

1920
def handle_response(self, response: HTTPResponse) -> ActivateNumberResponse:
20-
super(ActivateNumberEndpoint, self).handle_response(response)
21+
try:
22+
super(ActivateNumberEndpoint, self).handle_response(response)
23+
except NumbersException as ex:
24+
raise NumberNotFoundException(message=ex.args[0], response=ex.http_response,
25+
is_from_server=ex.is_from_server)
2126
return self.process_response_model(response.body, ActivateNumberResponse)

sinch/domains/numbers/endpoints/available/list_available_numbers.py sinch/domains/numbers/endpoints/available/list_available_numbers_endpoint.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
44
from sinch.domains.numbers.models.available.list_available_numbers_request import ListAvailableNumbersRequest
55
from sinch.domains.numbers.models.available.list_available_numbers_response import ListAvailableNumbersResponse
6+
from sinch.domains.numbers.models.numbers import Number
67

78

89
class AvailableNumbersEndpoint(NumbersEndpoint):
@@ -15,30 +16,26 @@ class AvailableNumbersEndpoint(NumbersEndpoint):
1516

1617
def __init__(self, project_id: str, request_data: ListAvailableNumbersRequest):
1718
super(AvailableNumbersEndpoint, self).__init__(project_id, request_data)
19+
self.request_data = request_data
1820

1921
def build_query_params(self) -> dict:
20-
"""
21-
Constructs the query parameters for the endpoint.
22-
23-
Returns:
24-
dict: The query parameters to be sent with the API request.
25-
"""
2622
# Serialize fields
2723
query_params = self.request_data.model_dump(exclude_none=True, by_alias=True)
2824
return query_params
2925

3026
def request_body(self):
3127
pass
3228

33-
def handle_response(self, response: HTTPResponse) -> ListAvailableNumbersResponse:
29+
def handle_response(self, response: HTTPResponse) -> list[Number]:
3430
"""
3531
Processes the API response and maps it to a response model.
3632
3733
Args:
3834
response (HTTPResponse): The raw HTTP response object received from the API.
3935
4036
Returns:
41-
ListAvailableNumbersResponse: The response model containing the parsed response data.
37+
list[Number]: The response model containing the parsed response data.
4238
"""
4339
super(AvailableNumbersEndpoint, self).handle_response(response)
44-
return self.process_response_model(response.body, ListAvailableNumbersResponse)
40+
response = self.process_response_model(response.body, ListAvailableNumbersResponse)
41+
return response.available_numbers

sinch/domains/numbers/endpoints/available/rent_any_number.py sinch/domains/numbers/endpoints/available/rent_any_number_endpoint.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class RentAnyNumberEndpoint(NumbersEndpoint):
1515

1616
def __init__(self, project_id: str, request_data: RentAnyNumberRequest):
1717
super(RentAnyNumberEndpoint, self).__init__(project_id, request_data)
18+
self.request_data = request_data
1819

1920
def handle_response(self, response: HTTPResponse) -> RentAnyNumberResponse:
2021
"""
@@ -26,5 +27,7 @@ def handle_response(self, response: HTTPResponse) -> RentAnyNumberResponse:
2627
Returns:
2728
RentAnyNumberResponse: The response data mapped to the RentAnyNumberResponse model.
2829
"""
29-
super(RentAnyNumberEndpoint, self).handle_response(response)
30+
error = super(RentAnyNumberEndpoint, self).handle_response(response)
31+
if error:
32+
return error
3033
return self.process_response_model(response.body, RentAnyNumberResponse)

sinch/domains/numbers/endpoints/available/search_for_number.py sinch/domains/numbers/endpoints/available/search_for_number_endpoint.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from sinch.core.models.http_response import HTTPResponse
22
from sinch.domains.numbers.endpoints.numbers_endpoint import NumbersEndpoint
33
from sinch.core.enums import HTTPAuthentication, HTTPMethods
4+
from sinch.domains.numbers.exceptions import NumberNotFoundException, NumbersException
45
from sinch.domains.numbers.models.available.check_number_availability_response import CheckNumberAvailabilityResponse
56
from sinch.domains.numbers.models.available.check_number_availability_request import CheckNumberAvailabilityRequest
67

@@ -30,5 +31,8 @@ def handle_response(self, response: HTTPResponse) -> CheckNumberAvailabilityResp
3031
CheckNumberAvailabilityResponse: The response model containing the parsed response data
3132
of the requested phone number.
3233
"""
33-
super(SearchForNumberEndpoint, self).handle_response(response)
34+
try:
35+
super(SearchForNumberEndpoint, self).handle_response(response)
36+
except NumbersException as e:
37+
raise NumberNotFoundException(message=e.args[0], response=e.http_response, is_from_server=e.is_from_server)
3438
return self.process_response_model(response.body, CheckNumberAvailabilityResponse)

sinch/domains/numbers/endpoints/numbers_endpoint.py

+12-20
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,19 @@
11
import json
2+
from abc import ABC
23
from pydantic import BaseModel
4+
from typing import TypeVar, Type
35
from sinch.core.models.http_response import HTTPResponse
46
from sinch.core.endpoint import HTTPEndpoint
57
from sinch.domains.numbers.exceptions import NumbersException
8+
from sinch.domains.numbers.models.numbers import NotFoundError
69

710

8-
class NumbersEndpoint(HTTPEndpoint):
9-
"""
10-
A base class for all endpoints, providing reusable logic for URL building
11-
and response parsing.
12-
"""
13-
ENDPOINT_URL: str = ""
14-
HTTP_METHOD: str = ""
15-
HTTP_AUTHENTICATION: str = ""
11+
class NumbersEndpoint(HTTPEndpoint, ABC):
1612

1713
def __init__(self, project_id: str, request_data: object):
18-
self.project_id = project_id
19-
self.request_data = request_data
14+
super().__init__(project_id, request_data)
2015

2116
def build_url(self, sinch) -> str:
22-
"""
23-
Constructs the URL for the endpoint.
24-
25-
Args:
26-
sinch: The Sinch client instance.
27-
28-
Returns:
29-
str: Fully constructed endpoint URL.
30-
"""
3117
if not self.ENDPOINT_URL:
3218
raise NotImplementedError("ENDPOINT_URL must be defined in the subclass.")
3319

@@ -48,7 +34,9 @@ def request_body(self):
4834
request_data = self.request_data.model_dump(by_alias=True, exclude_none=True)
4935
return json.dumps(request_data)
5036

51-
def process_response_model(self, response_body: dict, response_model: type[BaseModel]) -> BaseModel:
37+
BM = TypeVar("BM", bound=BaseModel)
38+
39+
def process_response_model(self, response_body: dict, response_model: Type[BM]) -> BM:
5240
"""
5341
Processes the response body and maps it to a response model.
5442
@@ -65,6 +53,10 @@ def process_response_model(self, response_body: dict, response_model: type[BaseM
6553
raise ValueError(f"Invalid response structure: {e}") from e
6654

6755
def handle_response(self, response: HTTPResponse):
56+
if response.status_code == 404:
57+
error = NotFoundError(**response.body['error'])
58+
raise NumbersException(message=error, response=response, is_from_server=True)
59+
6860
if response.status_code >= 400:
6961
raise NumbersException(
7062
message=f"{response.body['error'].get('message')} {response.body['error'].get('status')}",

sinch/domains/numbers/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class NumbersException(SinchException):
55
pass
6+
7+
8+
class NumberNotFoundException(NumbersException):
9+
pass

0 commit comments

Comments
 (0)