Skip to content

Commit f242754

Browse files
committed
test(provider): unleash integration test use testcontainers
Signed-off-by: Kiki L Hakiem <[email protected]>
1 parent 34b6f03 commit f242754

File tree

5 files changed

+249
-78
lines changed

5 files changed

+249
-78
lines changed

providers/openfeature-provider-unleash/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dev = [
3131
"mypy[faster-cache]>=1.17.0,<2.0.0",
3232
"pytest>=8.4.0,<9.0.0",
3333
"pytest-asyncio>=0.23.0",
34+
"psycopg2-binary>=2.9.0,<3.0.0",
35+
"testcontainers>=4.12.0,<5.0.0",
3436
"types-requests>=2.31.0",
3537
]
3638

providers/openfeature-provider-unleash/tests/conftest.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

providers/openfeature-provider-unleash/tests/test_integration.py

Lines changed: 149 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,78 @@
1-
"""Integration tests for Unleash provider using a running Unleash instance."""
1+
"""Integration tests for Unleash provider using testcontainers."""
2+
3+
from datetime import datetime, timezone
4+
import time
25

36
from openfeature import api
47
from openfeature.contrib.provider.unleash import UnleashProvider
58
from openfeature.evaluation_context import EvaluationContext
9+
import psycopg2
610
import pytest
711
import requests
12+
from testcontainers.core.container import DockerContainer
13+
from testcontainers.postgres import PostgresContainer
814

9-
10-
UNLEASH_URL = "http://0.0.0.0:4242/api"
15+
# Configuration for the running Unleash instance (will be set by fixtures)
16+
UNLEASH_URL = None
1117
API_TOKEN = "default:development.unleash-insecure-api-token"
1218
ADMIN_TOKEN = "user:76672ac99726f8e48a1bbba16b7094a50d1eee3583d1e8457e12187a"
1319

1420

21+
class UnleashContainer(DockerContainer):
22+
"""Custom Unleash container with health check."""
23+
24+
def __init__(self, postgres_url: str, **kwargs):
25+
super().__init__("unleashorg/unleash-server:latest", **kwargs)
26+
self.postgres_url = postgres_url
27+
28+
def _configure(self):
29+
self.with_env("DATABASE_URL", self.postgres_url)
30+
self.with_env("DATABASE_URL_FILE", "")
31+
self.with_env("DATABASE_SSL", "false")
32+
self.with_env("DATABASE_SSL_REJECT_UNAUTHORIZED", "false")
33+
self.with_env("LOG_LEVEL", "info")
34+
self.with_env("PORT", "4242")
35+
self.with_env("HOST", "0.0.0.0")
36+
self.with_env("ADMIN_AUTHENTICATION", "none")
37+
self.with_env("AUTH_ENABLE", "false")
38+
self.with_env("INIT_CLIENT_API_TOKENS", API_TOKEN)
39+
# Expose the Unleash port
40+
self.with_exposed_ports(4242)
41+
42+
43+
def insert_admin_token(postgres_container):
44+
"""Insert admin token into the Unleash database."""
45+
url = postgres_container.get_connection_url()
46+
conn = psycopg2.connect(url)
47+
48+
try:
49+
with conn.cursor() as cursor:
50+
cursor.execute(
51+
"""
52+
INSERT INTO "public"."personal_access_tokens"
53+
("secret", "description", "user_id", "expires_at", "seen_at", "created_at", "id")
54+
VALUES (%s, %s, %s, %s, %s, %s, %s)
55+
ON CONFLICT (id) DO NOTHING
56+
""",
57+
(
58+
"user:76672ac99726f8e48a1bbba16b7094a50d1eee3583d1e8457e12187a",
59+
"my-token",
60+
1,
61+
datetime(3025, 1, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
62+
datetime.now(timezone.utc),
63+
datetime.now(timezone.utc),
64+
1,
65+
),
66+
)
67+
conn.commit()
68+
print("Admin token inserted successfully")
69+
except Exception as e:
70+
print(f"Error inserting admin token: {e}")
71+
conn.rollback()
72+
finally:
73+
conn.close()
74+
75+
1576
def create_test_flags():
1677
"""Create test flags in the Unleash instance."""
1778
flags = [
@@ -58,7 +119,7 @@ def create_test_flags():
58119
for flag in flags:
59120
try:
60121
response = requests.post(
61-
f"{UNLEASH_URL}/admin/projects/default/features",
122+
f"{UNLEASH_URL}/api/admin/projects/default/features",
62123
headers=headers,
63124
json=flag,
64125
timeout=10,
@@ -157,7 +218,7 @@ def add_strategy_with_variants(flag_name: str, headers: dict):
157218
}
158219

159220
strategy_response = requests.post(
160-
f"{UNLEASH_URL}/admin/projects/default/features/{flag_name}/environments/development/strategies",
221+
f"{UNLEASH_URL}/api/admin/projects/default/features/{flag_name}/environments/development/strategies",
161222
headers=headers,
162223
json=strategy_payload,
163224
timeout=10,
@@ -174,7 +235,7 @@ def enable_flag(flag_name: str, headers: dict):
174235
"""Enable a flag in the development environment."""
175236
try:
176237
enable_response = requests.post(
177-
f"{UNLEASH_URL}/admin/projects/default/features/{flag_name}/environments/development/on",
238+
f"{UNLEASH_URL}/api/admin/projects/default/features/{flag_name}/environments/development/on",
178239
headers=headers,
179240
timeout=10,
180241
)
@@ -186,19 +247,95 @@ def enable_flag(flag_name: str, headers: dict):
186247
print(f"Error enabling flag '{flag_name}': {e}")
187248

188249

250+
@pytest.fixture(scope="session")
251+
def postgres_container():
252+
"""Create and start PostgreSQL container."""
253+
with PostgresContainer("postgres:15", driver=None) as postgres:
254+
postgres.start()
255+
postgres_url = postgres.get_connection_url()
256+
print(f"PostgreSQL started at: {postgres_url}")
257+
258+
yield postgres
259+
260+
261+
@pytest.fixture(scope="session")
262+
def unleash_container(postgres_container):
263+
"""Create and start Unleash container with PostgreSQL dependency."""
264+
global UNLEASH_URL
265+
266+
postgres_url = postgres_container.get_connection_url()
267+
postgres_bridge_ip = postgres_container.get_docker_client().bridge_ip(
268+
postgres_container._container.id
269+
)
270+
271+
# Create internal URL using the bridge IP and internal port (5432)
272+
exposed_port = postgres_container.get_exposed_port(5432)
273+
internal_url = postgres_url.replace("localhost", postgres_bridge_ip).replace(
274+
f":{exposed_port}", ":5432"
275+
)
276+
277+
unleash = UnleashContainer(internal_url)
278+
279+
with unleash as container:
280+
print("Starting Unleash container...")
281+
container.start()
282+
print("Unleash container started")
283+
284+
# Wait for health check to pass
285+
print("Waiting for Unleash container to be healthy...")
286+
max_wait_time = 60 # 1 minute max wait
287+
start_time = time.time()
288+
289+
while time.time() - start_time < max_wait_time:
290+
try:
291+
# Get the exposed port
292+
try:
293+
exposed_port = container.get_exposed_port(4242)
294+
unleash_url = f"http://localhost:{exposed_port}"
295+
print(f"Trying health check at: {unleash_url}")
296+
except Exception as port_error:
297+
print(f"Port not ready yet: {port_error}")
298+
time.sleep(2)
299+
continue
300+
301+
# Try to connect to health endpoint
302+
response = requests.get(f"{unleash_url}/health", timeout=5)
303+
if response.status_code == 200:
304+
print("Unleash container is healthy!")
305+
break
306+
307+
print(f"Health check failed, status: {response.status_code}")
308+
time.sleep(2)
309+
310+
except Exception as e:
311+
print(f"Health check error: {e}")
312+
time.sleep(2)
313+
else:
314+
raise Exception("Unleash container did not become healthy within timeout")
315+
316+
# Get the exposed port and set global URL
317+
UNLEASH_URL = f"http://localhost:{container.get_exposed_port(4242)}"
318+
print(f"Unleash started at: {unleash_url}")
319+
320+
insert_admin_token(postgres_container)
321+
print("Admin token inserted into database")
322+
323+
yield container, unleash_url
324+
325+
189326
@pytest.fixture(scope="session", autouse=True)
190-
def setup_test_flags():
327+
def setup_test_flags(unleash_container):
191328
"""Setup test flags before running any tests."""
192329
print("Creating test flags in Unleash...")
193330
create_test_flags()
194331
print("Test flags setup completed")
195332

196333

197-
@pytest.fixture
198-
def unleash_provider():
334+
@pytest.fixture(scope="session")
335+
def unleash_provider(setup_test_flags):
199336
"""Create an Unleash provider instance for testing."""
200337
provider = UnleashProvider(
201-
url=UNLEASH_URL,
338+
url=f"{UNLEASH_URL}/api",
202339
app_name="test-app",
203340
api_token=API_TOKEN,
204341
)
@@ -208,7 +345,7 @@ def unleash_provider():
208345
provider.shutdown()
209346

210347

211-
@pytest.fixture
348+
@pytest.fixture(scope="session")
212349
def client(unleash_provider):
213350
"""Create an OpenFeature client with the Unleash provider."""
214351
# Set the provider globally
@@ -219,24 +356,10 @@ def client(unleash_provider):
219356
@pytest.mark.integration
220357
def test_integration_health_check():
221358
"""Test that Unleash health check endpoint is accessible."""
222-
response = requests.get(f"{UNLEASH_URL.replace('/api', '')}/health", timeout=5)
359+
response = requests.get(f"{UNLEASH_URL}/health", timeout=5)
223360
assert response.status_code == 200
224361

225362

226-
@pytest.mark.integration
227-
def test_integration_provider_initialization(unleash_provider):
228-
"""Test that the Unleash provider can be initialized."""
229-
assert unleash_provider is not None
230-
assert unleash_provider.client is not None
231-
232-
233-
@pytest.mark.integration
234-
def test_integration_provider_metadata(unleash_provider):
235-
"""Test that the provider returns correct metadata."""
236-
metadata = unleash_provider.get_metadata()
237-
assert metadata.name == "Unleash Provider"
238-
239-
240363
@pytest.mark.integration
241364
def test_integration_flag_details_resolution(unleash_provider):
242365
"""Test flag details resolution with the Unleash provider."""
@@ -250,13 +373,6 @@ def test_integration_flag_details_resolution(unleash_provider):
250373
assert details.value is True
251374

252375

253-
@pytest.mark.integration
254-
def test_integration_provider_status(unleash_provider):
255-
"""Test that the provider status is correctly reported."""
256-
status = unleash_provider.get_status()
257-
assert status.value == "READY"
258-
259-
260376
@pytest.mark.integration
261377
def test_integration_boolean_flag_resolution(unleash_provider):
262378
"""Test boolean flag resolution with the Unleash provider."""

providers/openfeature-provider-unleash/tests/test_provider.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,27 +59,35 @@ def test_unleash_provider_initialization():
5959

6060
def test_unleash_provider_all_methods_implemented():
6161
"""Test that all required methods are implemented."""
62-
provider = UnleashProvider(
63-
url="http://localhost:4242", app_name="test-app", api_token="test-token"
64-
)
65-
provider.initialize()
66-
67-
# Test that all required methods exist
68-
assert hasattr(provider, "get_metadata")
69-
assert hasattr(provider, "resolve_boolean_details")
70-
assert hasattr(provider, "resolve_string_details")
71-
assert hasattr(provider, "resolve_integer_details")
72-
assert hasattr(provider, "resolve_float_details")
73-
assert hasattr(provider, "resolve_object_details")
74-
assert hasattr(provider, "initialize")
75-
assert hasattr(provider, "get_status")
76-
assert hasattr(provider, "shutdown")
77-
assert hasattr(provider, "on_context_changed")
78-
assert hasattr(provider, "add_handler")
79-
assert hasattr(provider, "remove_handler")
80-
assert hasattr(provider, "track")
62+
mock_client = Mock()
63+
mock_client.initialize_client.return_value = None
8164

82-
provider.shutdown()
65+
with patch(
66+
"openfeature.contrib.provider.unleash.UnleashClient"
67+
) as mock_unleash_client:
68+
mock_unleash_client.return_value = mock_client
69+
70+
provider = UnleashProvider(
71+
url="http://localhost:4242", app_name="test-app", api_token="test-token"
72+
)
73+
provider.initialize()
74+
75+
# Test that all required methods exist
76+
assert hasattr(provider, "get_metadata")
77+
assert hasattr(provider, "resolve_boolean_details")
78+
assert hasattr(provider, "resolve_string_details")
79+
assert hasattr(provider, "resolve_integer_details")
80+
assert hasattr(provider, "resolve_float_details")
81+
assert hasattr(provider, "resolve_object_details")
82+
assert hasattr(provider, "initialize")
83+
assert hasattr(provider, "get_status")
84+
assert hasattr(provider, "shutdown")
85+
assert hasattr(provider, "on_context_changed")
86+
assert hasattr(provider, "add_handler")
87+
assert hasattr(provider, "remove_handler")
88+
assert hasattr(provider, "track")
89+
90+
provider.shutdown()
8391

8492

8593
def test_unleash_provider_hooks():

0 commit comments

Comments
 (0)