""" Unit tests for Terminal Command History API endpoints. Tests cover: - GET /api/terminal/{host_id}/command-history - GET /api/terminal/{host_id}/command-history/unique - GET /api/terminal/command-history (global) - DELETE /api/terminal/{host_id}/command-history - POST /api/terminal/command-history/purge """ import pytest from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.models.terminal_command_log import TerminalCommandLog class TestGetHostCommandHistory: """Test GET /api/terminal/{host_id}/command-history endpoint.""" @pytest.mark.asyncio async def test_returns_command_history(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test successful retrieval of command history.""" # Create test data from app.crud.terminal_command_log import TerminalCommandLogRepository from app.crud.host import HostRepository # First ensure we have a host host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert "commands" in data assert "total" in data assert isinstance(data["commands"], list) @pytest.mark.asyncio async def test_returns_404_for_unknown_host(self, client: AsyncClient, auth_headers): """Test 404 response for non-existent host.""" response = await client.get( "/api/terminal/nonexistent-host-id/command-history", headers=auth_headers, ) assert response.status_code == 404 @pytest.mark.asyncio async def test_filters_by_query(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test search query filtering.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history?query=ls", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["query"] == "ls" @pytest.mark.asyncio async def test_respects_limit(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test limit parameter.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history?limit=10", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert len(data["commands"]) <= 10 @pytest.mark.asyncio async def test_caps_limit_at_100(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test that limit is capped at 100.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history?limit=500", headers=auth_headers, ) assert response.status_code == 200 # Should still work, just capped internally class TestGetHostUniqueCommands: """Test GET /api/terminal/{host_id}/command-history/unique endpoint.""" @pytest.mark.asyncio async def test_returns_unique_commands(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test retrieval of unique commands.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history/unique", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert "commands" in data assert "total" in data assert "host_id" in data class TestGetGlobalCommandHistory: """Test GET /api/terminal/command-history endpoint.""" @pytest.mark.asyncio async def test_returns_global_history(self, client: AsyncClient, auth_headers): """Test retrieval of global command history.""" response = await client.get( "/api/terminal/command-history", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert "commands" in data assert isinstance(data["commands"], list) @pytest.mark.asyncio async def test_filters_by_host_id(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test filtering by host_id.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/command-history?host_id={host_id}", headers=auth_headers, ) assert response.status_code == 200 data = response.json() assert data["host_id"] == host_id class TestClearHostCommandHistory: """Test DELETE /api/terminal/{host_id}/command-history endpoint.""" @pytest.mark.asyncio async def test_requires_admin_role(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test that only admins can clear history.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id # This test depends on the auth setup # If the test user is admin, it should succeed # If not admin, it should return 403 response = await client.delete( f"/api/terminal/{host_id}/command-history", headers=auth_headers, ) # Either 200 (admin) or 403 (not admin) is acceptable assert response.status_code in [200, 403] @pytest.mark.asyncio async def test_returns_404_for_unknown_host(self, client: AsyncClient, auth_headers): """Test 404 for non-existent host.""" response = await client.delete( "/api/terminal/nonexistent-host-id/command-history", headers=auth_headers, ) # Either 404 (host not found) or 403 (not admin) is acceptable assert response.status_code in [403, 404] class TestPurgeCommandHistory: """Test POST /api/terminal/command-history/purge endpoint.""" @pytest.mark.asyncio async def test_requires_admin_role(self, client: AsyncClient, auth_headers): """Test that only admins can purge history.""" response = await client.post( "/api/terminal/command-history/purge?days=30", headers=auth_headers, ) # Either 200 (admin) or 403 (not admin) is acceptable assert response.status_code in [200, 403] @pytest.mark.asyncio async def test_accepts_days_parameter(self, client: AsyncClient, auth_headers): """Test days parameter is accepted.""" response = await client.post( "/api/terminal/command-history/purge?days=7", headers=auth_headers, ) # Either 200 (admin) or 403 (not admin) is acceptable assert response.status_code in [200, 403] class TestCommandHistorySchemas: """Test schema validation for command history responses.""" @pytest.mark.asyncio async def test_command_history_item_schema(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test that response matches CommandHistoryItem schema.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history", headers=auth_headers, ) assert response.status_code == 200 data = response.json() for cmd in data["commands"]: assert "id" in cmd assert "command" in cmd assert "created_at" in cmd @pytest.mark.asyncio async def test_unique_command_item_schema(self, client: AsyncClient, db_session: AsyncSession, auth_headers): """Test that response matches UniqueCommandItem schema.""" from app.crud.host import HostRepository host_repo = HostRepository(db_session) hosts = await host_repo.list() if hosts: host_id = hosts[0].id response = await client.get( f"/api/terminal/{host_id}/command-history/unique", headers=auth_headers, ) assert response.status_code == 200 data = response.json() for cmd in data["commands"]: assert "command" in cmd assert "command_hash" in cmd assert "last_used" in cmd assert "execution_count" in cmd