Skip to content

Commit 0d68b90

Browse files
committed
Improve test coverage
1 parent de398b6 commit 0d68b90

File tree

4 files changed

+151
-18
lines changed

4 files changed

+151
-18
lines changed

backend/api/app/helpers/query.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ def check_ports(address: str, ports: list[int]) -> list[dict[str, int | bool]]:
120120
list[dict[str, int | bool]]:
121121
A list of dictionaries containing the ports checked and their statuses.
122122
"""
123+
# Explicit type check for ports
124+
for port in ports:
125+
if not isinstance(port, int):
126+
raise TypeError(f"Port '{port}' is not an integer.")
123127
results = []
124128
with ThreadPoolExecutor() as executor:
125129
futures = {

backend/tests/test_api_routes.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def test_route_health_check(client):
1010
assert response.status_code == HTTP_200_OK
1111
assert response.text == "true"
1212

13+
1314
def test_my_ip_endpoint(client, mocker):
1415
"""Test my_ip endpoint returns correct requester IP."""
1516
mock_get_requester = mocker.patch(
@@ -89,8 +90,12 @@ def test_query_post_endpoint_invalid_port_v1(client):
8990
assert ret["extra"][0]["message"] == "Input should be less than or equal to 65535"
9091

9192

92-
def test_query_post_endpoint_invalid_hostname_v1(client):
93+
def test_query_post_endpoint_invalid_hostname_v1(client, mocker):
9394
"""Test v1 query endpoint raises error with invalid hostname."""
95+
mocker.patch(
96+
"socket.gethostbyname",
97+
side_effect=OSError("Hostname does not appear to resolve"),
98+
)
9499
request_data = {"host": INVALID_HOST, "ports": [80]} # Invalid host
95100
path = "/api/v1/query"
96101
response = client.post(path, json=request_data)
@@ -134,8 +139,12 @@ def test_query_post_endpoint_invalid_port_v2(client):
134139
assert ret["extra"][0]["message"] == "Input should be less than or equal to 65535"
135140

136141

137-
def test_query_post_endpoint_invalid_hostname_v2(client):
142+
def test_query_post_endpoint_invalid_hostname_v2(client, mocker):
138143
"""Test v2 query endpoint raises error with invalid hostname."""
144+
mocker.patch(
145+
"socket.gethostbyname",
146+
side_effect=OSError("Hostname does not appear to resolve"),
147+
)
139148
request_data = {"host": INVALID_HOST, "ports": [80]} # Invalid host
140149
path = "/api/query"
141150
response = client.post(path, json=request_data)

backend/tests/test_query_address.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,84 @@
2020
)
2121

2222

23-
def test_query_address_single_closed_port():
24-
"""Mock socket connection to return non-zero, indicating the port is closed"""
25-
result = query_address(VALID_PUBLIC_IPV4, [CLOSED_PORTS[0]])
26-
assert result == [{"port": CLOSED_PORTS[0], "status": False}]
23+
@pytest.mark.parametrize(
24+
"host,ports,expected",
25+
[
26+
(
27+
VALID_PUBLIC_IPV4,
28+
[CLOSED_PORTS[0]],
29+
[{"port": CLOSED_PORTS[0], "status": False}],
30+
),
31+
(
32+
VALID_PUBLIC_IPV4,
33+
OPEN_PORTS,
34+
[{"port": port, "status": True} for port in OPEN_PORTS],
35+
),
36+
(
37+
VALID_PUBLIC_IPV4,
38+
CLOSED_PORTS,
39+
[{"port": port, "status": False} for port in CLOSED_PORTS],
40+
),
41+
(VALID_PUBLIC_IPV4, [], []),
42+
],
43+
)
44+
def test_query_address_various(host, ports, expected):
45+
"""Test query_address for various valid scenarios."""
46+
assert query_address(host, ports) == expected
2747

2848

2949
def test_query_address_multiple_ports_mixed_status():
3050
"""Test when some ports are open and some are closed."""
3151
result = query_address(VALID_PUBLIC_IPV4, PORTS)
3252
expected = [
33-
{
34-
"port": port,
35-
"status": mock_connect((VALID_PUBLIC_IPV4, port)) == SOCKET_OPEN,
36-
}
53+
{"port": port, "status": mock_connect((VALID_PUBLIC_IPV4, port)) == SOCKET_OPEN}
3754
for port in PORTS
3855
]
3956
assert result == expected
4057

4158

42-
def test_query_address_empty_ports_list():
43-
"""Test query_address returns empty list when ports list is empty."""
44-
ports = expected_result = []
45-
assert query_address(VALID_PUBLIC_IPV4, ports) == expected_result
46-
47-
48-
def test_query_address_invalid_address():
49-
"""Test query_address raises JsonAPIException for an invalid hostname."""
59+
def test_query_address_invalid_hostname(mocker):
60+
"""Test query_address raises JsonAPIException for an invalid hostname (mocked DNS failure)."""
61+
mocker.patch(
62+
"socket.gethostbyname",
63+
side_effect=OSError("Hostname does not appear to resolve"),
64+
)
5065
with pytest.raises(JsonAPIException, match=".*Hostname does not appear to resolve"):
5166
query_address(INVALID_HOST, [OPEN_PORTS[0]])
5267

5368

69+
def test_query_address_empty_host():
70+
"""Test query_address raises JsonAPIException for empty host."""
71+
with pytest.raises(JsonAPIException, match="A hostname must be provided"):
72+
query_address("", [80])
73+
74+
75+
def test_query_address_none_host():
76+
"""Test query_address raises JsonAPIException for None as host."""
77+
with pytest.raises(JsonAPIException):
78+
query_address(None, [80])
79+
80+
81+
def test_query_address_invalid_port_type():
82+
"""Test query_address raises TypeError for non-integer port."""
83+
with pytest.raises(Exception):
84+
query_address(VALID_PUBLIC_IPV4, ["notaport"])
85+
86+
87+
def test_query_address_ipv6():
88+
"""Test query_address raises JsonAPIException for IPv6 address."""
89+
with pytest.raises(JsonAPIException, match="IPv6 is not currently supported"):
90+
query_address("2001:4860:4860::8888", [80])
91+
92+
93+
def test_query_address_duplicate_ports():
94+
"""Test query_address handles duplicate ports gracefully."""
95+
ports = [80, 80, 443]
96+
result = query_address(VALID_PUBLIC_IPV4, ports)
97+
assert result.count({"port": 80, "status": True}) == 2
98+
assert {"port": 443, "status": True} in result
99+
100+
54101
def test_check_ports_all_open():
55102
"""Test when all ports are open."""
56103
result = check_ports(VALID_PUBLIC_IPV4, OPEN_PORTS)

backend/tests/test_schemas_api.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Tests for schemas"""
2+
import pytest
3+
from pydantic import ValidationError
4+
5+
from api.app.schemas.api import (
6+
APICheckSchema,
7+
APIErrorResponseSchema,
8+
APIResponseSchema,
9+
APISchema,
10+
)
11+
12+
# Dummy values for annotations
13+
HOST = "example.com"
14+
PORT = 443
15+
16+
17+
def test_apischema_valid():
18+
"""Test that APISchema validates and parses correct input data."""
19+
data = {"host": HOST, "ports": [PORT, 80]}
20+
schema = APISchema(**data)
21+
assert schema.host == HOST
22+
assert schema.ports == [PORT, 80]
23+
24+
25+
def test_apischema_missing_host():
26+
"""Test that APISchema raises ValidationError if 'host' is missing."""
27+
data = {"ports": [PORT]}
28+
with pytest.raises(ValidationError):
29+
APISchema(**data)
30+
31+
32+
def test_apicheckschema_valid():
33+
"""Test that APICheckSchema validates and parses correct input data."""
34+
data = {"port": PORT, "status": True}
35+
schema = APICheckSchema(**data)
36+
assert schema.port == PORT
37+
assert schema.status is True
38+
39+
40+
def test_apiresponseschema_valid():
41+
"""Test that APIResponseSchema validates and parses correct input data."""
42+
check = {"port": PORT, "status": True}
43+
data = {
44+
"error": False,
45+
"msg": "OK",
46+
"check": [check],
47+
"host": HOST,
48+
}
49+
schema = APIResponseSchema(**data)
50+
assert schema.error is False
51+
assert schema.msg == "OK"
52+
assert schema.host == HOST
53+
assert schema.check[0].port == PORT
54+
55+
56+
def test_apierrorresponseschema_valid():
57+
"""Test that APIErrorResponseSchema validates and parses correct input data."""
58+
data = {
59+
"error": True,
60+
"detail": "fail",
61+
"extra": [{"param": "host", "error": "invalid"}],
62+
}
63+
schema = APIErrorResponseSchema(**data)
64+
assert schema.error is True
65+
assert schema.detail == "fail"
66+
assert schema.extra[0]["param"] == "host"
67+
68+
69+
def test_apierrorresponseschema_missing_fields():
70+
"""Test that APIErrorResponseSchema raises ValidationError if required fields are missing."""
71+
data = {"error": True}
72+
with pytest.raises(ValidationError):
73+
APIErrorResponseSchema(**data)

0 commit comments

Comments
 (0)