11"""
22Integration tests for CTS API endpoints.
33
4- Run with pytest in one of two modes :
4+ Run with pytest by setting the CTS_URL environment variable :
55
6- 1. Direct HTTP mode (local testing):
7- CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v
6+ CTS_URL=http://localhost:5005 pytest tests/test_integration_api.py -v
87
9- 2. Kubectl exec mode (CI testing):
10- KUBECTL_POD=cts-abc123 pytest tests/test_integration_api.py -v
11-
12- Uses kubectl exec to run curl inside the CTS pod. URLs are properly quoted
13- to handle special characters like & in query parameters.
8+ In CI, the tests run in a pod deployed to the same namespace as the CTS service,
9+ so they can access it directly via: http://cts:5005
1410"""
1511
1612import json
1713import os
18- import subprocess
1914from urllib .request import urlopen , Request
2015from urllib .error import HTTPError , URLError
2116
2217import pytest
2318
2419
2520class HTTPClient :
26- """HTTP client that works in both direct and kubectl exec modes """
21+ """Simple HTTP client for making requests to the CTS API """
2722
28- def __init__ (self , base_url = None , kubectl_pod = None ):
29- self .base_url = base_url .rstrip ("/" ) if base_url else None
30- self .kubectl_pod = kubectl_pod
23+ def __init__ (self , base_url ):
24+ self .base_url = base_url .rstrip ("/" )
3125
3226 def _request (self , method , path , json_data = None ):
3327 """Make HTTP request with specified method"""
34- if self .kubectl_pod :
35- # Kubectl exec mode - use curl
36- # Important: Quote the URL to prevent shell interpretation of & and other special chars
37- url = f"http://localhost:5005{ path } "
38- if json_data :
39- json_str = json .dumps (json_data ).replace ("'" , "'\\ ''" )
40- cmd = f"curl -s -w '\\ n%{{http_code}}' -X { method } -H 'Content-Type: application/json' -d '{ json_str } ' '{ url } '"
41- else :
42- cmd = f"curl -s -w '\\ n%{{http_code}}' -X { method } '{ url } '"
43-
44- result = subprocess .run (
45- ["kubectl" , "exec" , "-i" , self .kubectl_pod , "--" , "sh" , "-c" , cmd ],
46- capture_output = True ,
47- text = True ,
48- )
49-
50- # Parse output: last line is status code, rest is body
51- lines = result .stdout .rsplit ("\n " , 1 )
52- if len (lines ) == 2 :
53- body , status_code = lines
54- try :
55- status = int (status_code )
56- except ValueError :
57- body = result .stdout
58- status = 200 if result .returncode == 0 else 500
59- else :
60- body = result .stdout
61- status = 200 if result .returncode == 0 else 500
62-
63- # Try to parse as JSON
64- try :
65- return status , json .loads (body ) if body .strip () else {}
66- except json .JSONDecodeError :
67- return status , body
68- else :
69- # Direct HTTP mode
70- url = f"{ self .base_url } { path } "
71- req = Request (url , method = method )
72- if json_data :
73- req .add_header ("Content-Type" , "application/json" )
74- req .data = json .dumps (json_data ).encode ("utf-8" )
75-
28+ url = f"{ self .base_url } { path } "
29+ req = Request (url , method = method )
30+ if json_data :
31+ req .add_header ("Content-Type" , "application/json" )
32+ req .data = json .dumps (json_data ).encode ("utf-8" )
33+
34+ try :
35+ with urlopen (req , timeout = 10 ) as response :
36+ data = response .read ()
37+ if response .headers .get ("Content-Type" , "" ).startswith (
38+ "application/json"
39+ ):
40+ return response .status , json .loads (data )
41+ return response .status , data .decode ("utf-8" )
42+ except HTTPError as e :
43+ # Try to read error body
7644 try :
77- with urlopen (req , timeout = 10 ) as response :
78- data = response .read ()
79- if response .headers .get ("Content-Type" , "" ).startswith (
80- "application/json"
81- ):
82- return response .status , json .loads (data )
83- return response .status , data .decode ("utf-8" )
84- except HTTPError as e :
85- # Try to read error body
86- try :
87- error_data = e .read ()
88- if e .headers .get ("Content-Type" , "" ).startswith ("application/json" ):
89- return e .code , json .loads (error_data )
90- return e .code , error_data .decode ("utf-8" )
91- except :
92- return e .code , None
93- except URLError as e :
94- raise Exception (f"Failed to connect to { url } : { e } " )
45+ error_data = e .read ()
46+ if e .headers .get ("Content-Type" , "" ).startswith ("application/json" ):
47+ return e .code , json .loads (error_data )
48+ return e .code , error_data .decode ("utf-8" )
49+ except :
50+ return e .code , None
51+ except URLError as e :
52+ raise Exception (f"Failed to connect to { url } : { e } " )
9553
9654 def get (self , path ):
9755 """Make HTTP GET request"""
@@ -112,18 +70,14 @@ def delete(self, path):
11270
11371@pytest .fixture (scope = "module" )
11472def http_client ():
115- """HTTP client fixture that auto-detects mode from environment"""
116- kubectl_pod = os .environ .get ("KUBECTL_POD" )
73+ """HTTP client fixture that reads CTS_URL from environment"""
11774 base_url = os .environ .get ("CTS_URL" )
11875
119- if kubectl_pod :
120- print (f"\n Using kubectl exec mode (pod: { kubectl_pod } )" )
121- return HTTPClient (kubectl_pod = kubectl_pod )
122- elif base_url :
123- print (f"\n Using direct HTTP mode (URL: { base_url } )" )
124- return HTTPClient (base_url = base_url )
125- else :
126- pytest .skip ("Must set either CTS_URL or KUBECTL_POD environment variable" )
76+ if not base_url :
77+ pytest .skip ("Must set CTS_URL environment variable" )
78+
79+ print (f"\n Connecting to CTS at: { base_url } " )
80+ return HTTPClient (base_url = base_url )
12781
12882
12983def _create_compose_info (
0 commit comments