Skip to content

Commit bbb13aa

Browse files
test: add unit tests for continents data and endpoints
1 parent d96611e commit bbb13aa

2 files changed

Lines changed: 352 additions & 0 deletions

File tree

data/tests/test_continents.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""
2+
Tests for the continents data module.
3+
"""
4+
import pytest
5+
from unittest.mock import patch, MagicMock
6+
from data import continents
7+
8+
9+
class TestContinents:
10+
"""Test class for continents module."""
11+
12+
def test_get_continents(self):
13+
"""Test getting all continents."""
14+
with patch('data.db_connect.read') as mock_read:
15+
mock_read.return_value = [continents.TEST_CONTINENT]
16+
result = continents.get_continents()
17+
mock_read.assert_called_once_with(continents.CONTINENTS_COLLECT)
18+
assert result == [continents.TEST_CONTINENT]
19+
20+
def test_get_continent_by_name_found(self):
21+
"""Test getting a continent by name when it exists."""
22+
with patch('data.db_connect.read_one') as mock_read_one:
23+
mock_read_one.return_value = continents.TEST_CONTINENT
24+
result = continents.get_continent_by_name('North America')
25+
mock_read_one.assert_called_once_with(
26+
continents.CONTINENTS_COLLECT,
27+
{continents.CONTINENT_NAME: 'North America'}
28+
)
29+
assert result == continents.TEST_CONTINENT
30+
31+
def test_get_continent_by_name_not_found(self):
32+
"""Test getting a continent by name when it does not exist."""
33+
with patch('data.db_connect.read_one') as mock_read_one:
34+
mock_read_one.return_value = None
35+
result = continents.get_continent_by_name('Atlantis')
36+
assert result is None
37+
38+
def test_add_continent_success(self):
39+
"""Test successfully adding a new continent."""
40+
with patch('data.continents.get_continent_by_name') as mock_get, \
41+
patch('data.db_connect.create') as mock_create:
42+
mock_get.return_value = None
43+
mock_result = MagicMock()
44+
mock_result.acknowledged = True
45+
mock_create.return_value = mock_result
46+
47+
data = {continents.CONTINENT_NAME: 'North America'}
48+
result = continents.add_continent(data)
49+
50+
assert result is True
51+
mock_create.assert_called_once()
52+
doc = mock_create.call_args[0][1]
53+
assert 'created_at' in doc and 'updated_at' in doc
54+
import datetime as _dt
55+
assert isinstance(doc['created_at'], _dt.datetime)
56+
assert isinstance(doc['updated_at'], _dt.datetime)
57+
58+
def test_add_continent_missing_required_field(self):
59+
"""Test adding a continent without continent_name raises ValueError."""
60+
with pytest.raises(ValueError, match="Missing required field"):
61+
continents.add_continent({})
62+
63+
def test_add_continent_invalid_name(self):
64+
"""Test adding a continent with an invalid name raises ValueError."""
65+
with pytest.raises(ValueError, match="Invalid continent"):
66+
continents.add_continent({continents.CONTINENT_NAME: 'Atlantis'})
67+
68+
def test_add_continent_already_exists(self):
69+
"""Test adding a continent that already exists raises ValueError."""
70+
with patch('data.continents.get_continent_by_name') as mock_get:
71+
mock_get.return_value = continents.TEST_CONTINENT
72+
with pytest.raises(ValueError, match="already exists"):
73+
continents.add_continent({continents.CONTINENT_NAME: 'North America'})
74+
75+
def test_add_continent_strips_timestamps(self):
76+
"""Test that client-supplied timestamps are overwritten."""
77+
with patch('data.continents.get_continent_by_name') as mock_get, \
78+
patch('data.db_connect.create') as mock_create:
79+
mock_get.return_value = None
80+
ack = MagicMock()
81+
ack.acknowledged = True
82+
mock_create.return_value = ack
83+
84+
data = {
85+
continents.CONTINENT_NAME: 'Asia',
86+
'created_at': '1999-01-01',
87+
'updated_at': '1999-01-01',
88+
}
89+
continents.add_continent(data)
90+
91+
doc = mock_create.call_args[0][1]
92+
assert doc['created_at'] != '1999-01-01'
93+
assert doc['updated_at'] != '1999-01-01'
94+
95+
def test_update_continent_success(self):
96+
"""Test successfully updating a continent."""
97+
with patch('data.continents.get_continent_by_name') as mock_get, \
98+
patch('data.db_connect.update') as mock_update:
99+
mock_get.return_value = continents.TEST_CONTINENT
100+
mock_result = MagicMock()
101+
mock_result.modified_count = 1
102+
mock_update.return_value = mock_result
103+
104+
result = continents.update_continent('North America', {})
105+
assert result is True
106+
mock_update.assert_called_once()
107+
108+
def test_update_continent_not_found(self):
109+
"""Test updating a continent that does not exist returns False."""
110+
with patch('data.continents.get_continent_by_name') as mock_get:
111+
mock_get.return_value = None
112+
result = continents.update_continent('Atlantis', {})
113+
assert result is False
114+
115+
def test_update_continent_name_not_changed(self):
116+
"""Test that update_continent keeps the original name regardless of input."""
117+
with patch('data.continents.get_continent_by_name') as mock_get, \
118+
patch('data.db_connect.update') as mock_update:
119+
mock_get.return_value = continents.TEST_CONTINENT
120+
mock_result = MagicMock()
121+
mock_result.modified_count = 1
122+
mock_update.return_value = mock_result
123+
124+
continents.update_continent('North America', {continents.CONTINENT_NAME: 'Asia'})
125+
126+
doc = mock_update.call_args[0][2]
127+
assert doc[continents.CONTINENT_NAME] == 'North America'
128+
129+
def test_delete_continent_success(self):
130+
"""Test successfully deleting a continent with no dependent countries."""
131+
with patch('data.countries.get_countries_by_continent') as mock_countries, \
132+
patch('data.db_connect.delete') as mock_delete:
133+
mock_countries.return_value = []
134+
mock_delete.return_value = 1
135+
136+
result = continents.delete_continent('Antarctica')
137+
assert result is True
138+
mock_delete.assert_called_once_with(
139+
continents.CONTINENTS_COLLECT,
140+
{continents.CONTINENT_NAME: 'Antarctica'}
141+
)
142+
143+
def test_delete_continent_not_found(self):
144+
"""Test deleting a continent that does not exist returns False."""
145+
with patch('data.countries.get_countries_by_continent') as mock_countries, \
146+
patch('data.db_connect.delete') as mock_delete:
147+
mock_countries.return_value = []
148+
mock_delete.return_value = 0
149+
150+
result = continents.delete_continent('Antarctica')
151+
assert result is False
152+
153+
def test_delete_continent_with_countries(self):
154+
"""Test that deleting a continent with countries raises ValueError."""
155+
with patch('data.countries.get_countries_by_continent') as mock_countries:
156+
mock_countries.return_value = [{'country_name': 'Canada'}, {'country_name': 'USA'}]
157+
158+
with pytest.raises(ValueError, match="Cannot delete"):
159+
continents.delete_continent('North America')
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""
2+
Tests for continents API endpoints.
3+
"""
4+
import json
5+
import pytest
6+
from unittest.mock import patch
7+
from http import HTTPStatus
8+
from server.app import create_app
9+
from data import continents
10+
11+
12+
class TestContinentsEndpoints:
13+
"""Test class for continents API endpoints."""
14+
15+
@pytest.fixture
16+
def client(self):
17+
"""Create a test client."""
18+
app = create_app()
19+
app.config['TESTING'] = True
20+
with app.test_client() as client:
21+
yield client
22+
23+
@pytest.fixture
24+
def sample_continent(self):
25+
return {'continent_name': 'Asia'}
26+
27+
# --- GET /continents ---
28+
29+
def test_get_all_continents_success(self, client):
30+
"""GET /continents returns list of all continents."""
31+
with patch('data.continents.get_continents') as mock_get:
32+
mock_get.return_value = [continents.TEST_CONTINENT]
33+
response = client.get('/continents')
34+
assert response.status_code == HTTPStatus.OK
35+
data = response.get_json()
36+
assert isinstance(data, list)
37+
assert len(data) == 1
38+
mock_get.assert_called_once()
39+
40+
def test_get_all_continents_with_pagination(self, client):
41+
"""GET /continents supports limit and offset query parameters."""
42+
two_continents = [
43+
{'continent_name': 'Africa'},
44+
{'continent_name': 'Asia'},
45+
]
46+
with patch('data.continents.get_continents') as mock_get:
47+
mock_get.return_value = two_continents
48+
response = client.get('/continents?limit=1&offset=1')
49+
assert response.status_code == HTTPStatus.OK
50+
data = response.get_json()
51+
assert len(data) == 1
52+
assert data[0]['continent_name'] == 'Asia'
53+
54+
def test_get_all_continents_invalid_limit(self, client):
55+
"""GET /continents?limit=-1 returns 400."""
56+
response = client.get('/continents?limit=-1')
57+
assert response.status_code == HTTPStatus.BAD_REQUEST
58+
59+
def test_get_all_continents_db_error(self, client):
60+
"""GET /continents returns 500 on database error."""
61+
with patch('data.continents.get_continents') as mock_get:
62+
mock_get.side_effect = Exception('DB connection failed')
63+
response = client.get('/continents')
64+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
65+
66+
# --- POST /continents ---
67+
68+
def test_create_continent_success(self, client, sample_continent):
69+
"""POST /continents creates a new continent and returns 201."""
70+
with patch('data.continents.add_continent') as mock_add:
71+
mock_add.return_value = True
72+
response = client.post(
73+
'/continents',
74+
data=json.dumps(sample_continent),
75+
content_type='application/json'
76+
)
77+
assert response.status_code == HTTPStatus.CREATED
78+
data = response.get_json()
79+
assert data['continent_name'] == 'Asia'
80+
mock_add.assert_called_once_with(sample_continent)
81+
82+
def test_create_continent_invalid_name(self, client):
83+
"""POST /continents with invalid continent_name returns 400."""
84+
response = client.post(
85+
'/continents',
86+
data=json.dumps({'continent_name': 'Atlantis'}),
87+
content_type='application/json'
88+
)
89+
assert response.status_code == HTTPStatus.BAD_REQUEST
90+
91+
def test_create_continent_already_exists(self, client, sample_continent):
92+
"""POST /continents with duplicate name returns 409."""
93+
with patch('data.continents.add_continent') as mock_add:
94+
mock_add.side_effect = ValueError("Continent 'Asia' already exists")
95+
response = client.post(
96+
'/continents',
97+
data=json.dumps(sample_continent),
98+
content_type='application/json'
99+
)
100+
assert response.status_code == HTTPStatus.CONFLICT
101+
102+
def test_create_continent_db_error(self, client, sample_continent):
103+
"""POST /continents returns 500 on unexpected database error."""
104+
with patch('data.continents.add_continent') as mock_add:
105+
mock_add.side_effect = Exception('DB write failed')
106+
response = client.post(
107+
'/continents',
108+
data=json.dumps(sample_continent),
109+
content_type='application/json'
110+
)
111+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
112+
113+
# --- GET /continents/<name> ---
114+
115+
def test_get_continent_success(self, client):
116+
"""GET /continents/<name> returns the continent."""
117+
with patch('data.continents.get_continent_by_name') as mock_get:
118+
mock_get.return_value = continents.TEST_CONTINENT
119+
response = client.get('/continents/North America')
120+
assert response.status_code == HTTPStatus.OK
121+
data = response.get_json()
122+
assert data['continent_name'] == continents.TEST_CONTINENT['continent_name']
123+
124+
def test_get_continent_not_found(self, client):
125+
"""GET /continents/<name> returns 404 when not found."""
126+
with patch('data.continents.get_continent_by_name') as mock_get:
127+
mock_get.return_value = None
128+
response = client.get('/continents/Atlantis')
129+
assert response.status_code == HTTPStatus.NOT_FOUND
130+
131+
def test_get_continent_db_error(self, client):
132+
"""GET /continents/<name> returns 500 on database error."""
133+
with patch('data.continents.get_continent_by_name') as mock_get:
134+
mock_get.side_effect = Exception('DB error')
135+
response = client.get('/continents/Asia')
136+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
137+
138+
# --- PUT /continents/<name> ---
139+
140+
def test_update_continent_success(self, client):
141+
"""PUT /continents/<name> returns 204 on success."""
142+
with patch('data.continents.update_continent') as mock_update:
143+
mock_update.return_value = True
144+
response = client.put('/continents/Asia')
145+
assert response.status_code == HTTPStatus.NO_CONTENT
146+
147+
def test_update_continent_not_found(self, client):
148+
"""PUT /continents/<name> returns 404 when continent does not exist."""
149+
with patch('data.continents.update_continent') as mock_update:
150+
mock_update.return_value = False
151+
response = client.put('/continents/Atlantis')
152+
assert response.status_code == HTTPStatus.NOT_FOUND
153+
154+
def test_update_continent_db_error(self, client):
155+
"""PUT /continents/<name> returns 500 on database error."""
156+
with patch('data.continents.update_continent') as mock_update:
157+
mock_update.side_effect = Exception('DB error')
158+
response = client.put('/continents/Asia')
159+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
160+
161+
# --- DELETE /continents/<name> ---
162+
163+
def test_delete_continent_success(self, client):
164+
"""DELETE /continents/<name> returns 204 on success."""
165+
with patch('data.continents.delete_continent') as mock_delete:
166+
mock_delete.return_value = True
167+
response = client.delete('/continents/Antarctica')
168+
assert response.status_code == HTTPStatus.NO_CONTENT
169+
170+
def test_delete_continent_not_found(self, client):
171+
"""DELETE /continents/<name> returns 404 when not found."""
172+
with patch('data.continents.delete_continent') as mock_delete:
173+
mock_delete.return_value = False
174+
response = client.delete('/continents/Atlantis')
175+
assert response.status_code == HTTPStatus.NOT_FOUND
176+
177+
def test_delete_continent_with_countries(self, client):
178+
"""DELETE /continents/<name> returns 409 when countries reference it."""
179+
with patch('data.continents.delete_continent') as mock_delete:
180+
mock_delete.side_effect = ValueError(
181+
'Cannot delete: 3 country/countries reference this continent'
182+
)
183+
response = client.delete('/continents/Africa')
184+
assert response.status_code == HTTPStatus.CONFLICT
185+
data = response.get_json()
186+
assert 'Cannot delete' in data['message']
187+
188+
def test_delete_continent_db_error(self, client):
189+
"""DELETE /continents/<name> returns 500 on unexpected error."""
190+
with patch('data.continents.delete_continent') as mock_delete:
191+
mock_delete.side_effect = Exception('DB error')
192+
response = client.delete('/continents/Asia')
193+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR

0 commit comments

Comments
 (0)