Skip to content

Commit 2ca8963

Browse files
committed
feat[tests]: Add extensive integration and unit tests for OpenStack VM repository and API routes
- Added pytest-based integration tests for OpenStack VM repository, including VM lifecycle, resource retrieval, and error handling. - Implemented unit tests for repository factory to validate mock and real repository creation logic. - Expanded error handling tests for VM API routes to improve endpoint coverage and exception handling scenarios.
1 parent b3d6451 commit 2ca8963

9 files changed

Lines changed: 1199 additions & 38 deletions

.coveragerc

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
[run]
2+
# Coverage configuration for OpenStack VM Lifecycle Management API
3+
4+
# Source directories to measure
5+
source = app
6+
7+
# Files to omit from coverage
8+
omit =
9+
*/tests/*
10+
*/__pycache__/*
11+
*/venv/*
12+
*/.venv/*
13+
*/site-packages/*
14+
*/__init__.py
15+
*/conftest.py
16+
# Omit OpenStack repository from unit test coverage
17+
# It's tested separately with integration tests (pytest -m integration)
18+
app/repositories/openstack_vm_repository.py
19+
# Omit test helpers
20+
tests/openstack_helpers.py
21+
22+
# Branch coverage (measure if/else branches)
23+
branch = True
24+
25+
[report]
26+
# Reporting options
27+
28+
# Show missing lines
29+
show_missing = True
30+
31+
# Precision for coverage percentage
32+
precision = 2
33+
34+
# Lines to exclude from coverage
35+
exclude_lines =
36+
# Standard pragma
37+
pragma: no cover
38+
39+
# Don't complain about missing debug code
40+
def __repr__
41+
def __str__
42+
43+
# Don't complain if tests don't hit defensive assertion code
44+
raise AssertionError
45+
raise NotImplementedError
46+
47+
# Don't complain if non-runnable code isn't run
48+
if __name__ == .__main__.:
49+
if TYPE_CHECKING:
50+
if typing.TYPE_CHECKING:
51+
52+
# Don't complain about abstract methods
53+
@abstractmethod
54+
@abc.abstractmethod
55+
56+
# Don't complain about overload definitions
57+
@overload
58+
59+
# Don't complain about ellipsis
60+
\.\.\.
61+
62+
# Note: OpenStack repository is tested with integration tests
63+
# Run with: USE_REAL_OPENSTACK=True pytest -m "" --cov=app
64+
# For unit tests only, OpenStack repository coverage is excluded below
65+
66+
# Fail if coverage is below this threshold
67+
# Unit tests target: 90% (excluding OpenStack repository)
68+
# All tests target: 85% (including OpenStack repository)
69+
# fail_under = 85.0 # Commented out - manually check coverage
70+
71+
[html]
72+
# HTML report configuration
73+
directory = htmlcov
74+
title = OpenStack VM API Coverage Report
75+
76+
[xml]
77+
# XML report for CI/CD
78+
output = coverage.xml

pytest.ini

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[pytest]
2+
# Pytest configuration for OpenStack VM Lifecycle Management API
3+
4+
# Test markers
5+
markers =
6+
unit: Unit tests that use mock repository (no external dependencies)
7+
integration: Integration tests that require real OpenStack connection
8+
slow: Tests that take longer to execute
9+
10+
# Default options
11+
# By default, run only unit tests (skip integration tests requiring OpenStack)
12+
addopts =
13+
-v
14+
--tb=short
15+
--cov=app
16+
--cov-report=term
17+
--cov-report=html
18+
-m "not integration"
19+
--strict-markers
20+
21+
# Test discovery patterns
22+
python_files = test_*.py
23+
python_classes = Test*
24+
python_functions = test_*
25+
26+
# Minimum Python version
27+
minversion = 3.11
28+
29+
# Test paths
30+
testpaths = tests
31+
32+
# Asyncio mode
33+
asyncio_mode = auto
34+
35+
# Coverage options
36+
# Note: Additional coverage configuration in .coveragerc

tests/conftest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
Test Configuration
3+
4+
Global pytest configuration and fixtures for test suite.
5+
6+
IMPORTANT: This module sets environment variables before any app imports.
7+
"""
8+
9+
import os
10+
11+
# Set test environment variables BEFORE any imports from app
12+
# This ensures settings are loaded with test configuration
13+
os.environ["USE_REAL_OPENSTACK"] = "False"
14+
os.environ["ENVIRONMENT"] = "test"
15+
os.environ["LOG_LEVEL"] = "WARNING"
16+
17+
import pytest # noqa: E402
18+
19+
20+
@pytest.fixture(scope="session", autouse=True)
21+
def set_test_environment():
22+
"""
23+
Verify test environment is set correctly.
24+
25+
Environment variables are set at module level before imports.
26+
"""
27+
# Verify settings
28+
from app.config import settings
29+
30+
assert settings.use_real_openstack is False, "Tests must use mock repository"
31+
assert settings.environment == "test", "Tests must run in test environment"
32+
33+
yield
34+
35+
# Cleanup after all tests
36+
# Reset to default values
37+
os.environ.pop("USE_REAL_OPENSTACK", None)
38+
os.environ.pop("ENVIRONMENT", None)
39+
os.environ.pop("LOG_LEVEL", None)

tests/openstack_helpers.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""
2+
OpenStack Test Helpers
3+
4+
Utility functions for OpenStack integration testing.
5+
"""
6+
7+
import logging
8+
from typing import List, Optional
9+
10+
import openstack
11+
from openstack.connection import Connection
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def create_openstack_connection() -> Connection:
17+
"""
18+
Create OpenStack connection for testing.
19+
20+
Returns:
21+
OpenStack connection object
22+
23+
Raises:
24+
RuntimeError: If connection fails
25+
"""
26+
try:
27+
import os
28+
29+
conn = openstack.connect(
30+
auth_url=os.getenv("OPENSTACK_AUTH_URL", "http://192.168.2.110/identity"),
31+
project_name=os.getenv("OPENSTACK_PROJECT_NAME", "demo"),
32+
username=os.getenv("OPENSTACK_USERNAME", "admin"),
33+
password=os.getenv("OPENSTACK_PASSWORD", "devstack"),
34+
user_domain_name=os.getenv("OPENSTACK_USER_DOMAIN_NAME", "Default"),
35+
project_domain_name=os.getenv("OPENSTACK_PROJECT_DOMAIN_NAME", "Default"),
36+
region_name=os.getenv("OPENSTACK_REGION_NAME", "RegionOne"),
37+
)
38+
logger.info("OpenStack connection established for testing")
39+
return conn
40+
except Exception as e:
41+
logger.error(f"Failed to create OpenStack connection: {e}")
42+
raise RuntimeError(f"OpenStack connection failed: {e}")
43+
44+
45+
def cleanup_test_vms(
46+
conn: Connection, prefix: str = "test-", max_age_seconds: Optional[int] = None
47+
):
48+
"""
49+
Clean up VMs created during testing.
50+
51+
Args:
52+
conn: OpenStack connection
53+
prefix: VM name prefix to identify test VMs
54+
max_age_seconds: Optional age threshold to delete older VMs
55+
56+
Returns:
57+
Number of VMs deleted
58+
"""
59+
try:
60+
servers = list(conn.compute.servers())
61+
deleted_count = 0
62+
63+
for server in servers:
64+
if server.name.startswith(prefix):
65+
try:
66+
logger.info(f"Deleting test VM: {server.name} (ID: {server.id})")
67+
conn.compute.delete_server(server, wait=True)
68+
deleted_count += 1
69+
except Exception as e:
70+
logger.warning(f"Failed to delete VM {server.name}: {e}")
71+
72+
logger.info(f"Cleaned up {deleted_count} test VMs")
73+
return deleted_count
74+
except Exception as e:
75+
logger.error(f"Failed to cleanup test VMs: {e}")
76+
return 0
77+
78+
79+
def wait_for_server_status(
80+
conn: Connection, server_id: str, expected_status: str, timeout: int = 60
81+
) -> bool:
82+
"""
83+
Wait for server to reach expected status.
84+
85+
Args:
86+
conn: OpenStack connection
87+
server_id: Server ID to monitor
88+
expected_status: Expected status (ACTIVE, SHUTOFF, etc.)
89+
timeout: Maximum wait time in seconds
90+
91+
Returns:
92+
True if status reached, False on timeout
93+
"""
94+
try:
95+
server = conn.compute.get_server(server_id)
96+
if not server:
97+
return False
98+
99+
server = conn.compute.wait_for_server(
100+
server, status=expected_status, wait=timeout
101+
)
102+
return server.status == expected_status
103+
except Exception as e:
104+
logger.error(f"Error waiting for server status: {e}")
105+
return False
106+
107+
108+
def get_available_networks(conn: Connection) -> List[str]:
109+
"""
110+
Get list of available network names.
111+
112+
Args:
113+
conn: OpenStack connection
114+
115+
Returns:
116+
List of network names
117+
"""
118+
try:
119+
networks = list(conn.network.networks())
120+
network_names = [net.name for net in networks]
121+
logger.debug(f"Available networks: {network_names}")
122+
return network_names
123+
except Exception as e:
124+
logger.error(f"Failed to get networks: {e}")
125+
return []
126+
127+
128+
def get_available_images(conn: Connection) -> List[str]:
129+
"""
130+
Get list of available image names.
131+
132+
Args:
133+
conn: OpenStack connection
134+
135+
Returns:
136+
List of image names
137+
"""
138+
try:
139+
images = list(conn.compute.images())
140+
image_names = [img.name for img in images]
141+
logger.debug(f"Available images: {image_names}")
142+
return image_names
143+
except Exception as e:
144+
logger.error(f"Failed to get images: {e}")
145+
return []
146+
147+
148+
def get_available_flavors(conn: Connection) -> List[str]:
149+
"""
150+
Get list of available flavor names.
151+
152+
Args:
153+
conn: OpenStack connection
154+
155+
Returns:
156+
List of flavor names
157+
"""
158+
try:
159+
flavors = list(conn.compute.flavors())
160+
flavor_names = [f.name for f in flavors]
161+
logger.debug(f"Available flavors: {flavor_names}")
162+
return flavor_names
163+
except Exception as e:
164+
logger.error(f"Failed to get flavors: {e}")
165+
return []
166+
167+
168+
def verify_openstack_availability() -> bool:
169+
"""
170+
Verify that OpenStack is available and accessible.
171+
172+
Returns:
173+
True if OpenStack is available, False otherwise
174+
"""
175+
try:
176+
conn = create_openstack_connection()
177+
# Try a simple operation
178+
list(conn.compute.flavors(details=False, limit=1))
179+
logger.info("OpenStack is available and accessible")
180+
return True
181+
except Exception as e:
182+
logger.warning(f"OpenStack is not available: {e}")
183+
return False

0 commit comments

Comments
 (0)