Skip to content

Commit 141e28f

Browse files
authored
CM-22159 add unit tests coverage (#103)
1 parent 8937ae8 commit 141e28f

19 files changed

+652
-26
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[run]
2+
omit =
3+
# ignore all test cases in tests/
4+
tests/*

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
__pycache__
22
/build/
33
/*.egg-info/
4-
/dist/
4+
/dist/
5+
6+
# coverage
7+
.coverage
8+
htmlcov
9+
coverage.*

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ gitpython==3.1.30
1111
mock==4.0.3
1212
pytest==6.2.5
1313
pytest-mock==3.6.1
14+
coverage==7.2.3
15+
responses==0.23.1
1416
binaryornot==0.4.4
1517
halo==0.0.31
1618
texttable==1.6.7

tests/cli/__init__.py

Whitespace-only changes.

tests/cli/test_cycode.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import json
2+
3+
import pytest
4+
from typing import TYPE_CHECKING
5+
6+
import responses
7+
from click.testing import CliRunner
8+
9+
from cli.cycode import main_cli
10+
from tests.conftest import TEST_FILES_PATH, CLI_ENV_VARS
11+
from tests.cyclient.test_scan_client import get_zipped_file_scan_response, get_zipped_file_scan_url
12+
13+
_PATH_TO_SCAN = TEST_FILES_PATH.joinpath('zip_content').absolute()
14+
15+
if TYPE_CHECKING:
16+
from cyclient.scan_client import ScanClient
17+
18+
19+
def _is_json(plain: str) -> bool:
20+
try:
21+
json.loads(plain)
22+
return True
23+
except (ValueError, TypeError):
24+
return False
25+
26+
27+
@responses.activate
28+
@pytest.mark.parametrize('output', ['text', 'json'])
29+
def test_passing_output_option_to_scan(output: str, scan_client: 'ScanClient', api_token_response: responses.Response):
30+
scan_type = 'secret'
31+
32+
responses.add(get_zipped_file_scan_response(get_zipped_file_scan_url(scan_type, scan_client)))
33+
responses.add(api_token_response)
34+
# scan report is not mocked. This raise connection error on attempt to report scan. it doesn't perform real request
35+
36+
args = ['scan', '--soft-fail', '--output', output, 'path', str(_PATH_TO_SCAN)]
37+
result = CliRunner().invoke(main_cli, args, env=CLI_ENV_VARS)
38+
39+
except_json = output == 'json'
40+
41+
assert _is_json(result.output) == except_json
42+
43+
if except_json:
44+
output = json.loads(result.output)
45+
assert 'scan_id' in output
46+
else:
47+
assert 'Scan Results' in result.output

tests/conftest.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
import responses
5+
6+
from cyclient.cycode_token_based_client import CycodeTokenBasedClient
7+
from cyclient.scan_client import ScanClient
8+
from cyclient.scan_config.scan_config_creator import create_scan_client
9+
10+
_EXPECTED_API_TOKEN = 'someJWT'
11+
12+
_CLIENT_ID = 'b1234568-0eaa-1234-beb8-6f0c12345678'
13+
_CLIENT_SECRET = 'a12345a-42b2-1234-3bdd-c0130123456'
14+
15+
CLI_ENV_VARS = {
16+
'CYCODE_CLIENT_ID': _CLIENT_ID,
17+
'CYCODE_CLIENT_SECRET': _CLIENT_SECRET
18+
}
19+
20+
TEST_FILES_PATH = Path(__file__).parent.joinpath('test_files').absolute()
21+
22+
23+
@pytest.fixture(scope='session')
24+
def scan_client() -> ScanClient:
25+
return create_scan_client(_CLIENT_ID, _CLIENT_SECRET)
26+
27+
28+
@pytest.fixture(scope='session')
29+
def token_based_client() -> CycodeTokenBasedClient:
30+
return CycodeTokenBasedClient(_CLIENT_ID, _CLIENT_SECRET)
31+
32+
33+
@pytest.fixture(scope='session')
34+
def api_token_url(token_based_client: CycodeTokenBasedClient) -> str:
35+
return f'{token_based_client.api_url}/api/v1/auth/api-token'
36+
37+
38+
@pytest.fixture(scope='session')
39+
def api_token_response(api_token_url) -> responses.Response:
40+
return responses.Response(
41+
method=responses.POST,
42+
url=api_token_url,
43+
json={
44+
'token': _EXPECTED_API_TOKEN,
45+
'refresh_token': '12345678-0c68-1234-91ba-a13123456789',
46+
'expires_in': 86400
47+
},
48+
status=200
49+
)
50+
51+
52+
@pytest.fixture(scope='session')
53+
@responses.activate
54+
def api_token(token_based_client: CycodeTokenBasedClient, api_token_response: responses.Response) -> str:
55+
responses.add(api_token_response)
56+
return token_based_client.api_token

tests/cyclient/scan_config/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from cyclient.scan_config.scan_config_creator import DefaultScanConfig
2+
3+
4+
def test_get_service_name():
5+
default_scan_config = DefaultScanConfig()
6+
7+
assert default_scan_config.get_service_name('secret') == 'secret'
8+
assert default_scan_config.get_service_name('iac') == 'iac'
9+
assert default_scan_config.get_service_name('sca') == 'scans'
10+
assert default_scan_config.get_service_name('sast') == 'scans'
11+
12+
13+
def test_get_scans_prefix():
14+
default_scan_config = DefaultScanConfig()
15+
16+
assert default_scan_config.get_scans_prefix() == 'scans'
17+
18+
19+
def test_get_detections_prefix():
20+
default_scan_config = DefaultScanConfig()
21+
22+
assert default_scan_config.get_detections_prefix() == 'detections'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from cyclient.scan_config.scan_config_creator import DevScanConfig
2+
3+
4+
def test_get_service_name():
5+
dev_scan_config = DevScanConfig()
6+
7+
assert dev_scan_config.get_service_name('secret') == '5025'
8+
assert dev_scan_config.get_service_name('iac') == '5026'
9+
assert dev_scan_config.get_service_name('sca') == '5004'
10+
assert dev_scan_config.get_service_name('sast') == '5004'
11+
12+
13+
def test_get_scans_prefix():
14+
dev_scan_config = DevScanConfig()
15+
16+
assert dev_scan_config.get_scans_prefix() == '5004'
17+
18+
19+
def test_get_detections_prefix():
20+
dev_scan_config = DevScanConfig()
21+
22+
assert dev_scan_config.get_detections_prefix() == '5016'

tests/cyclient/test_auth_client.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import pytest
2+
import requests
3+
import responses
4+
from requests import Timeout
5+
6+
from cyclient.auth_client import AuthClient
7+
from cyclient.models import AuthenticationSession, ApiTokenGenerationPollingResponse, \
8+
ApiTokenGenerationPollingResponseSchema
9+
from cli.exceptions.custom_exceptions import CycodeError
10+
11+
12+
@pytest.fixture(scope='module')
13+
def code_challenge() -> str:
14+
from cli.auth.auth_manager import AuthManager
15+
code_challenge, _ = AuthManager()._generate_pkce_code_pair()
16+
return code_challenge
17+
18+
19+
@pytest.fixture(scope='module')
20+
def code_verifier() -> str:
21+
from cli.auth.auth_manager import AuthManager
22+
_, code_verifier = AuthManager()._generate_pkce_code_pair()
23+
return code_verifier
24+
25+
26+
@pytest.fixture(scope='module', name='client')
27+
def auth_client() -> AuthClient:
28+
return AuthClient()
29+
30+
31+
@pytest.fixture(scope='module', name='start_url')
32+
def auth_start_url(client: AuthClient) -> str:
33+
# TODO(MarshalX): create database of constants of endpoints. remove hardcoded paths
34+
return client.cycode_client.build_full_url(
35+
client.cycode_client.api_url,
36+
f'{client.AUTH_CONTROLLER_PATH}/start'
37+
)
38+
39+
40+
@pytest.fixture(scope='module', name='token_url')
41+
def auth_token_url(client: AuthClient) -> str:
42+
return client.cycode_client.build_full_url(
43+
client.cycode_client.api_url,
44+
f'{client.AUTH_CONTROLLER_PATH}/token'
45+
)
46+
47+
48+
_SESSION_ID = '4cff1234-a209-47ed-ab2f-85676912345c'
49+
50+
51+
@responses.activate
52+
def test_start_session_success(client: AuthClient, start_url: str, code_challenge: str):
53+
responses.add(
54+
responses.POST,
55+
start_url,
56+
json={'session_id': _SESSION_ID},
57+
status=200,
58+
)
59+
60+
session_response = client.start_session(code_challenge)
61+
assert isinstance(session_response, AuthenticationSession)
62+
assert session_response.session_id == _SESSION_ID
63+
64+
65+
@responses.activate
66+
def test_start_session_timeout(client: AuthClient, start_url: str, code_challenge: str):
67+
responses.add(responses.POST, start_url, status=504)
68+
69+
timeout_response = requests.post(start_url, timeout=5)
70+
if timeout_response.status_code == 504:
71+
"""bypass SAST"""
72+
73+
responses.reset()
74+
75+
timeout_error = Timeout()
76+
timeout_error.response = timeout_response
77+
78+
responses.add(responses.POST, start_url, body=timeout_error)
79+
80+
with pytest.raises(CycodeError) as e_info:
81+
client.start_session(code_challenge)
82+
83+
assert e_info.value.status_code == 504
84+
85+
86+
@responses.activate
87+
def test_start_session_http_error(client: AuthClient, start_url: str, code_challenge: str):
88+
responses.add(responses.POST, start_url, status=401)
89+
90+
with pytest.raises(CycodeError) as e_info:
91+
client.start_session(code_challenge)
92+
93+
assert e_info.value.status_code == 401
94+
95+
96+
@responses.activate
97+
def test_get_api_token_success_pending(client: AuthClient, token_url: str, code_verifier: str):
98+
expected_status = 'Pending'
99+
expected_api_token = None
100+
101+
responses.add(
102+
responses.POST,
103+
token_url,
104+
json={'status': expected_status, 'api_token': expected_api_token},
105+
status=200,
106+
)
107+
108+
api_token_polling_response = client.get_api_token(_SESSION_ID, code_verifier)
109+
assert isinstance(api_token_polling_response, ApiTokenGenerationPollingResponse)
110+
assert api_token_polling_response.status == expected_status
111+
assert api_token_polling_response.api_token == expected_api_token
112+
113+
114+
@responses.activate
115+
def test_get_api_token_success_completed(client: AuthClient, token_url: str, code_verifier: str):
116+
expected_status = 'Completed'
117+
expected_json = {
118+
'status': expected_status,
119+
'api_token': {
120+
'clientId': 'b123458-0eaa-4010-beb4-6f0c54612345',
121+
'secret': 'a123450a-42b2-4ad5-8bdd-c0130123456',
122+
'description': 'cycode cli api token',
123+
'createdByUserId': None,
124+
'createdAt': '2023-04-26T11:38:54+00:00'
125+
}
126+
}
127+
expected_response = ApiTokenGenerationPollingResponseSchema().load(expected_json)
128+
129+
responses.add(
130+
responses.POST,
131+
token_url,
132+
json=expected_json,
133+
status=200,
134+
)
135+
136+
api_token_polling_response = client.get_api_token(_SESSION_ID, code_verifier)
137+
assert isinstance(api_token_polling_response, ApiTokenGenerationPollingResponse)
138+
assert api_token_polling_response.status == expected_status
139+
assert api_token_polling_response.api_token.client_id == expected_response.api_token.client_id
140+
assert api_token_polling_response.api_token.secret == expected_response.api_token.secret
141+
assert api_token_polling_response.api_token.description == expected_response.api_token.description
142+
143+
144+
@responses.activate
145+
def test_get_api_token_http_error_valid_response(client: AuthClient, token_url: str, code_verifier: str):
146+
# TODO(MarshalX): ask Michal about such cases or dive into code of platform
147+
expected_status = 'Pending'
148+
expected_api_token = None
149+
150+
responses.add(
151+
responses.POST,
152+
token_url,
153+
json={'status': expected_status, 'api_token': expected_api_token},
154+
status=418, # any code between 400 and 600
155+
)
156+
157+
api_token_polling_response = client.get_api_token(_SESSION_ID, code_verifier)
158+
assert isinstance(api_token_polling_response, ApiTokenGenerationPollingResponse)
159+
assert api_token_polling_response.status == expected_status
160+
assert api_token_polling_response.api_token == expected_api_token
161+
162+
163+
@responses.activate
164+
def test_get_api_token_http_error_invalid_response(client: AuthClient, token_url: str, code_verifier: str):
165+
responses.add(
166+
responses.POST,
167+
token_url,
168+
body='Invalid body',
169+
status=418, # any code between 400 and 600
170+
)
171+
172+
api_token_polling_response = client.get_api_token(_SESSION_ID, code_verifier)
173+
assert api_token_polling_response is None
174+
175+
176+
@responses.activate
177+
def test_get_api_token_not_excepted_exception(client: AuthClient, token_url: str, code_verifier: str):
178+
responses.add(responses.POST, token_url, body=Timeout())
179+
180+
api_token_polling_response = client.get_api_token(_SESSION_ID, code_verifier)
181+
assert api_token_polling_response is None

0 commit comments

Comments
 (0)