""" Tests for routes/containers.py — container, image, and volume management endpoints. All endpoints require is_local_request() to return True; non-local gets 403. """ import sys import json from pathlib import Path from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, str(Path(__file__).parent.parent / 'api')) import app as app_module from app import app @pytest.fixture def local_client(): """Flask test client that appears as local (loopback) request.""" app.config['TESTING'] = True with app.test_client() as c: # is_local_request is imported inside each route handler from app with patch.object(app_module, 'is_local_request', return_value=True): yield c @pytest.fixture def remote_client(): """Flask test client that appears as a non-local (remote) request.""" app.config['TESTING'] = True with app.test_client() as c: with patch.object(app_module, 'is_local_request', return_value=False): yield c # --------------------------------------------------------------------------- # GET /api/containers — requires local # --------------------------------------------------------------------------- class TestListContainers: def test_returns_200_from_local(self, local_client): mock_cm = MagicMock() mock_cm.list_containers.return_value = [{'name': 'cell-dns', 'status': 'running'}] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers') assert resp.status_code == 200 def test_returns_containers_list(self, local_client): mock_cm = MagicMock() mock_cm.list_containers.return_value = [{'name': 'cell-dns'}] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers') data = json.loads(resp.data) assert isinstance(data, list) assert data[0]['name'] == 'cell-dns' def test_returns_403_from_remote(self, remote_client): resp = remote_client.get('/api/containers') assert resp.status_code == 403 def test_500_on_exception(self, local_client): mock_cm = MagicMock() mock_cm.list_containers.side_effect = Exception('docker error') with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers') assert resp.status_code == 500 # --------------------------------------------------------------------------- # POST /api/containers//start # --------------------------------------------------------------------------- class TestStartContainer: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.start_container.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers/cell-dns/start') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/containers/cell-dns/start') assert resp.status_code == 403 def test_response_shape(self, local_client): mock_cm = MagicMock() mock_cm.start_container.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers/cell-dns/start') data = json.loads(resp.data) assert 'started' in data # --------------------------------------------------------------------------- # POST /api/containers//stop # --------------------------------------------------------------------------- class TestStopContainer: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.stop_container.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers/cell-dns/stop') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/containers/cell-dns/stop') assert resp.status_code == 403 # --------------------------------------------------------------------------- # POST /api/containers//restart # --------------------------------------------------------------------------- class TestRestartContainer: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.restart_container.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers/cell-dns/restart') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/containers/cell-dns/restart') assert resp.status_code == 403 # --------------------------------------------------------------------------- # GET /api/containers//logs # --------------------------------------------------------------------------- class TestGetContainerLogs: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.get_container_logs.return_value = ['line1', 'line2'] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers/cell-dns/logs') assert resp.status_code == 200 def test_returns_logs_in_response(self, local_client): mock_cm = MagicMock() mock_cm.get_container_logs.return_value = ['line1'] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers/cell-dns/logs?tail=50') data = json.loads(resp.data) assert 'logs' in data def test_returns_403_from_remote(self, remote_client): resp = remote_client.get('/api/containers/cell-dns/logs') assert resp.status_code == 403 # --------------------------------------------------------------------------- # GET /api/containers//stats # --------------------------------------------------------------------------- class TestGetContainerStats: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.get_container_stats.return_value = {'cpu': '5%', 'memory': '100MB'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/containers/cell-dns/stats') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.get('/api/containers/cell-dns/stats') assert resp.status_code == 403 # --------------------------------------------------------------------------- # POST /api/containers (create) # --------------------------------------------------------------------------- class TestCreateContainer: def test_returns_400_when_image_missing(self, local_client): resp = local_client.post('/api/containers', data=json.dumps({'name': 'test'}), content_type='application/json') assert resp.status_code == 400 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/containers', data=json.dumps({'image': 'nginx'}), content_type='application/json') assert resp.status_code == 403 def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.create_container.return_value = {'id': 'abc123', 'name': 'mycontainer'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers', data=json.dumps({'image': 'nginx', 'name': 'mycontainer'}), content_type='application/json') assert resp.status_code == 200 def test_returns_500_when_result_has_error(self, local_client): mock_cm = MagicMock() mock_cm.create_container.return_value = {'error': 'image not found'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers', data=json.dumps({'image': 'badimage'}), content_type='application/json') assert resp.status_code == 500 def test_volume_outside_allowed_path_returns_403(self, local_client): """Volume mounts outside the allowed directories are blocked.""" mock_cm = MagicMock() with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers', data=json.dumps({ 'image': 'nginx', 'volumes': {'/etc/passwd': '/mnt/passwd'} }), content_type='application/json') assert resp.status_code == 403 def test_volume_in_allowed_path_passes(self, local_client): """Volume mounts under /tmp/ are permitted.""" mock_cm = MagicMock() mock_cm.create_container.return_value = {'id': 'abc'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/containers', data=json.dumps({ 'image': 'nginx', 'volumes': {'/tmp/test': '/mnt/test'} }), content_type='application/json') assert resp.status_code == 200 # --------------------------------------------------------------------------- # DELETE /api/containers/ # --------------------------------------------------------------------------- class TestRemoveContainer: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.remove_container.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.delete('/api/containers/mycontainer') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.delete('/api/containers/mycontainer') assert resp.status_code == 403 # --------------------------------------------------------------------------- # GET /api/images # --------------------------------------------------------------------------- class TestListImages: def test_returns_200(self, local_client): mock_cm = MagicMock() mock_cm.list_images.return_value = [{'tag': 'nginx:latest'}] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/images') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.get('/api/images') assert resp.status_code == 403 # --------------------------------------------------------------------------- # POST /api/images/pull # --------------------------------------------------------------------------- class TestPullImage: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.pull_image.return_value = {'status': 'pulled'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/images/pull', data=json.dumps({'image': 'nginx:latest'}), content_type='application/json') assert resp.status_code == 200 def test_returns_400_when_image_missing(self, local_client): resp = local_client.post('/api/images/pull', data=json.dumps({}), content_type='application/json') assert resp.status_code == 400 def test_returns_500_when_pull_fails(self, local_client): mock_cm = MagicMock() mock_cm.pull_image.return_value = {'error': 'not found'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/images/pull', data=json.dumps({'image': 'badimage'}), content_type='application/json') assert resp.status_code == 500 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/images/pull', data=json.dumps({'image': 'nginx'}), content_type='application/json') assert resp.status_code == 403 # --------------------------------------------------------------------------- # DELETE /api/images/ # --------------------------------------------------------------------------- class TestRemoveImage: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.remove_image.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.delete('/api/images/nginx:latest') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.delete('/api/images/nginx:latest') assert resp.status_code == 403 # --------------------------------------------------------------------------- # GET /api/volumes # --------------------------------------------------------------------------- class TestListVolumes: def test_returns_200(self, local_client): mock_cm = MagicMock() mock_cm.list_volumes.return_value = [] with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.get('/api/volumes') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.get('/api/volumes') assert resp.status_code == 403 # --------------------------------------------------------------------------- # POST /api/volumes (create) # --------------------------------------------------------------------------- class TestCreateVolume: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.create_volume.return_value = {'name': 'myvolume'} with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.post('/api/volumes', data=json.dumps({'name': 'myvolume'}), content_type='application/json') assert resp.status_code == 200 def test_returns_400_when_name_missing(self, local_client): resp = local_client.post('/api/volumes', data=json.dumps({}), content_type='application/json') assert resp.status_code == 400 def test_returns_403_from_remote(self, remote_client): resp = remote_client.post('/api/volumes', data=json.dumps({'name': 'v'}), content_type='application/json') assert resp.status_code == 403 # --------------------------------------------------------------------------- # DELETE /api/volumes/ # --------------------------------------------------------------------------- class TestRemoveVolume: def test_returns_200_on_success(self, local_client): mock_cm = MagicMock() mock_cm.remove_volume.return_value = True with patch.object(app_module, 'container_manager', mock_cm): resp = local_client.delete('/api/volumes/myvolume') assert resp.status_code == 200 def test_returns_403_from_remote(self, remote_client): resp = remote_client.delete('/api/volumes/myvolume') assert resp.status_code == 403