Issue: #237 - Make Bastion Host Usage Default When Creating VMs Version: 1.0 Date: 2025-10-31 Testing Approach: Test-Driven Development (TDD)
This document specifies comprehensive test cases for the Bastion Default feature following TDD principles. Tests are written FIRST and will FAIL until implementation is complete. This ensures all requirements are testable and the implementation meets acceptance criteria.
Testing Pyramid Distribution:
- Unit Tests: 60% (Fast, isolated, focused)
- Integration Tests: 30% (Module interactions)
- E2E Tests: 10% (Complete workflows)
As a developer, I want bastion hosts to be the default when creating VMs, so that my VMs are secure by default without manual configuration.
- AC1: Auto-detect bastion in resource group when creating VM
- AC2: Use bastion automatically if exists (after user confirmation)
- AC3: Prompt to create bastion if doesn't exist (default: yes)
- AC4: Allow user to decline and create public IP instead
- AC5: Maintain backward compatibility with
--use-bastionflag
Location: tests/unit/test_bastion_default_behavior.py
Test Classes:
TestBastionAutoDetection- Auto-detection logic (15 tests)TestUserPromptBehavior- User interaction (5 tests)TestVMProvisioningWithBastion- Provisioning logic (6 tests)TestBastionFlagOverride- Flag behavior (3 tests)TestBackwardCompatibility- Compatibility (4 tests)TestConnectionFlowWithBastion- Connection flow (3 tests)TestErrorHandling- Error scenarios (6 tests)TestBastionConfigPersistence- Config storage (3 tests)TestSecurityRequirements- Security validation (3 tests)TestBoundaryConditions- Edge cases (4 tests)
Total Unit Tests: ~52 tests
Location: tests/integration/test_bastion_default_integration.py
Test Classes:
TestBastionDetectionAndProvisioning- Detection + provisioning (4 tests)TestConnectionWorkflowWithBastion- Connection workflow (3 tests)TestBastionConfigPersistence- Config persistence (3 tests)TestCLIIntegration- CLI commands (3 tests)TestErrorRecoveryAndFallback- Error handling (3 tests)TestMultiVMScenarios- Multi-VM scenarios (2 tests)TestVNetPeeringScenarios- VNet peering (2 tests)
Total Integration Tests: ~20 tests
Location: tests/e2e/test_bastion_default_e2e.py
Test Classes:
TestAcceptanceCriteria- AC validation (5 tests)TestUserWorkflows- Complete workflows (6 tests)TestErrorScenarios- Error handling (3 tests)TestPerformanceAndScaling- Performance (2 tests)TestSecurityCompliance- Security (2 tests)
Total E2E Tests: ~18 tests
Grand Total: ~90 tests
Purpose: Verify bastion detection when bastion exists Priority: P0 (Critical) AC Coverage: AC1
def test_detect_bastion_in_resource_group_found(self):
# Arrange
resource_group = "my-rg"
mock_bastions = [{"name": "my-bastion", "resourceGroup": "my-rg"}]
# Act
with patch.object(BastionDetector, 'list_bastions', return_value=mock_bastions):
bastion = BastionDetector.detect_bastion_for_vm("test-vm", resource_group)
# Assert
assert bastion is not None
assert bastion["name"] == "my-bastion"Expected Result: Bastion is detected and returned Failure Mode: Returns None when bastion exists Implementation Hint: Query Azure API, filter by resource group
Purpose: Verify graceful handling when no bastion exists Priority: P0 (Critical) AC Coverage: AC1
Expected Result: Returns None without error Failure Mode: Raises exception or crashes Implementation Hint: Return None for empty list
Purpose: Verify behavior with multiple bastions Priority: P1 (High) AC Coverage: AC1
Expected Result: Uses first bastion in list Failure Mode: Uses wrong bastion or fails Implementation Hint: Take first element from sorted list
Purpose: Verify failed bastions are filtered out Priority: P0 (Critical) AC Coverage: AC1
Expected Result: Failed bastions are ignored Failure Mode: Attempts to use failed bastion Implementation Hint: Filter by provisioningState == "Succeeded"
Purpose: Verify error handling during detection Priority: P0 (Critical) AC Coverage: AC1
Expected Result: Returns None on Azure errors Failure Mode: Crashes or leaks error details Implementation Hint: Try-except with logging
Purpose: Verify user can accept using existing bastion Priority: P0 (Critical) AC Coverage: AC2
Expected Result: Returns True, bastion is used Failure Mode: Prompt not shown or ignored Implementation Hint: Use click.confirm()
Purpose: Verify default is 'yes' for creating bastion Priority: P0 (Critical) AC Coverage: AC3
Expected Result: Default=True in confirm prompt Failure Mode: Default is False or no default Implementation Hint: click.confirm(default=True)
Purpose: Verify cost info is shown to user Priority: P1 (High) AC Coverage: AC3
Expected Result: Prompt mentions ~$140/month cost Failure Mode: No cost information shown Implementation Hint: Include cost in prompt message
Purpose: Verify VM with bastion has no public IP Priority: P0 (Critical) AC Coverage: AC2
Expected Result: Public IP creation skipped Failure Mode: Public IP is created Implementation Hint: Omit --public-ip-address flag
Purpose: Verify VM without bastion gets public IP Priority: P0 (Critical) AC Coverage: AC4
Expected Result: Public IP is created Failure Mode: No public IP, VM unreachable Implementation Hint: Include --public-ip-address flag
Purpose: Verify auto-detection during provisioning Priority: P0 (Critical) AC Coverage: AC1, AC2
Expected Result: Bastion detected, user prompted, used Failure Mode: No detection or no prompt Implementation Hint: Call BastionDetector before provision
Purpose: Verify --no-bastion skips all detection Priority: P0 (Critical) AC Coverage: AC4
Expected Result: No detection calls made Failure Mode: Detection runs despite flag Implementation Hint: Check flag before detection
Purpose: Verify --no-bastion forces public IP Priority: P0 (Critical) AC Coverage: AC4
Expected Result: Public IP created regardless of bastion Failure Mode: Bastion used despite flag Implementation Hint: Override all bastion logic with flag
Purpose: Verify --use-bastion still works Priority: P0 (Critical) AC Coverage: AC5
Expected Result: Bastion used without prompt Failure Mode: Prompt shown or bastion not used Implementation Hint: Skip prompts when flag set
Purpose: Verify conflicting flags are rejected Priority: P1 (High) AC Coverage: AC5
Expected Result: ValueError raised Failure Mode: One flag silently ignored Implementation Hint: Validate flags in init
Purpose: Test complete detection and provision flow Priority: P0 (Critical) AC Coverage: AC1, AC2
Test Flow:
- BastionDetector queries Azure
- Bastion found in RG
- User prompted via click.confirm
- User accepts
- VM provisioned without public IP
- Bastion mapping saved to config
Expected Result: Complete flow succeeds Failure Mode: Any step fails or skipped Mock Points: Azure API, user input, subprocess
Purpose: Test bastion creation prompt flow Priority: P0 (Critical) AC Coverage: AC1, AC3
Test Flow:
- BastionDetector finds no bastion
- User prompted to create
- User accepts (default yes)
- Bastion creation initiated
- VM provisioned after bastion ready
Expected Result: Bastion created, VM uses it Failure Mode: No prompt or creation fails
Purpose: Test saved config is used for connections Priority: P1 (High) AC Coverage: AC2
Test Flow:
- Config has VM -> Bastion mapping
- User connects to VM
- BastionManager creates tunnel
- SSH connection via tunnel
- No user prompt (mapping exists)
Expected Result: Automatic connection via bastion Failure Mode: Prompt shown or direct connection
Purpose: Validate AC1 end-to-end Priority: P0 (Critical) AC Coverage: AC1
User Scenario:
# Resource group has bastion
azlin create my-vm --resource-group my-rg
# Expected:
# Detecting Bastion hosts in my-rg...
# Found: my-bastion
# Use this Bastion? (Y/n): [user accepts]
# Provisioning VM without public IP...Test Steps:
- Ensure RG has bastion (setup)
- Run azlin create command
- Verify detection message
- Verify prompt shown
- Accept prompt
- Verify VM created
- Verify no public IP
Pass Criteria: All steps succeed, VM uses bastion
Purpose: Validate AC2 end-to-end Priority: P0 (Critical) AC Coverage: AC2
User Scenario:
azlin create my-vm --resource-group my-rg
# Prompt: "Found Bastion 'my-bastion'. Use it? (Y/n)"
# User: Y
# VM created without public IPPass Criteria: Prompt shown, default is yes, VM uses bastion
Purpose: Validate AC3 end-to-end Priority: P0 (Critical) AC Coverage: AC3
User Scenario:
azlin create my-vm --resource-group new-rg
# Prompt: "No Bastion found. Create one? (Y/n) [~$140/month]"
# Default: Y
# User: [press enter]
# Creating Bastion... (10 minutes)
# VM created without public IPPass Criteria: Prompt shows cost, default yes, bastion created
Purpose: Validate AC4 end-to-end Priority: P0 (Critical) AC Coverage: AC4
User Scenario:
azlin create my-vm --resource-group my-rg
# Prompt: "Found Bastion. Use it? (Y/n)"
# User: n
# Creating VM with public IP...
# VM accessible at: 20.1.2.3Pass Criteria: User can decline, public IP created
Purpose: Validate AC5 end-to-end Priority: P0 (Critical) AC Coverage: AC5
User Scenario:
azlin connect my-vm --use-bastion
# No prompt (explicit flag)
# Creating Bastion tunnel...
# Connected to my-vm via BastionPass Criteria: Flag works, no breaking changes
Purpose: Test complete first-time user workflow Priority: P0 (Critical) AC Coverage: All
Workflow:
- New user, new resource group
- No bastion exists
- Prompted to create (accepts)
- Bastion created (~10 min)
- VM created without public IP
- Connection via bastion works
- Config saved for future use
Pass Criteria: Smooth workflow, all steps succeed
pytest tests/ -vpytest tests/unit/test_bastion_default_behavior.py -vpytest tests/integration/test_bastion_default_integration.py -vpytest tests/e2e/test_bastion_default_e2e.py -vexport RUN_E2E_TESTS=true
export AZLIN_E2E_RG=azlin-bastion-e2e
export AZLIN_E2E_LOCATION=westus2
pytest tests/e2e/test_bastion_default_e2e.py -v -m e2eMinimum Coverage: 90% for all critical paths
Critical Paths:
- Bastion detection logic
- User prompt handling
- VM provisioning with/without bastion
- Flag override behavior
- Config persistence
- Error handling
Coverage Command:
pytest --cov=azlin.modules.bastion_detector \
--cov=azlin.vm_provisioning \
--cov=azlin.vm_connector \
--cov-report=html \
--cov-report=term-missingRed-Green-Refactor Cycle:
-
Red: Write failing test
pytest tests/unit/test_bastion_default_behavior.py::TestBastionAutoDetection::test_detect_bastion_in_resource_group_found -v # FAILED - Function not implemented -
Green: Implement minimal code to pass
# azlin/modules/bastion_detector.py def detect_bastion_for_vm(vm_name, resource_group): bastions = list_bastions(resource_group) if bastions: return {"name": bastions[0]["name"], "resource_group": resource_group} return None
-
Refactor: Improve code quality
def detect_bastion_for_vm(vm_name, resource_group): """Detect bastion with error handling and logging.""" try: bastions = list_bastions(resource_group) active_bastions = [b for b in bastions if b.get("provisioningState") == "Succeeded"] if active_bastions: logger.info(f"Detected bastion: {active_bastions[0]['name']}") return {"name": active_bastions[0]["name"], "resource_group": resource_group} return None except Exception as e: logger.debug(f"Bastion detection failed: {e}") return None
-
Repeat: Next test in the suite
Bastion Response:
{
"name": "test-bastion",
"resourceGroup": "test-rg",
"location": "westus2",
"provisioningState": "Succeeded",
"sku": {"name": "Standard"},
"ipConfigurations": [{
"subnet": {"id": "/subscriptions/.../AzureBastionSubnet"}
}]
}VM Response:
{
"name": "test-vm",
"resourceGroup": "test-rg",
"location": "westus2",
"powerState": "VM running",
"privateIps": "10.0.0.4",
"publicIps": null
}Provided Fixtures:
mock_azure_resources- Azure API responsestemp_config_dir- Temporary config directorytest_env- Test environment variablesskip_e2e- Skip E2E unless enabled
All security tests from BASTION_SECURITY_REQUIREMENTS.md must pass:
-
REQ-CONFIG-001: No secrets in config
- Test:
test_no_bastion_credentials_stored - Verify: Config contains only names, no tokens
- Test:
-
REQ-CONFIG-002: Config file permissions
- Test:
test_bastion_config_file_permissions - Verify: Files are 0600 (owner read/write only)
- Test:
-
REQ-CONFIG-005: Bastion name validation
- Test:
test_bastion_name_validation_injection_prevention - Verify: Injection attempts are rejected
- Test:
Test Cases:
- Command injection via bastion name
- Path traversal via config file
- Credential leakage in error messages
- Tunnel hijacking attempts
Bastion Detection: <5 seconds VM Provisioning: <10 minutes (with bastion) Tunnel Creation: <30 seconds Config Load/Save: <100ms
def test_bastion_detection_performance():
start = time.time()
BastionDetector.detect_bastion_for_vm("test-vm", "test-rg")
duration = time.time() - start
assert duration < 5.0, f"Detection took {duration}s, expected <5s"- Weekly: Review failing tests
- Per PR: All tests must pass
- Monthly: Review test coverage
- Quarterly: Update test data
Test Debt Items:
- Real Azure E2E tests (currently mocked)
- VNet peering validation tests
- Bastion SKU compatibility tests
- Multi-region bastion tests
- Cost estimation tests
- All test files created
- Test specification reviewed
- Mock data prepared
- Fixtures implemented
- Security requirements understood
Phase 1: Detection (Week 1)
- Implement
BastionDetector.detect_bastion_for_vm() - Run:
pytest tests/unit/test_bastion_default_behavior.py::TestBastionAutoDetection -v - All detection tests should pass
Phase 2: Prompts (Week 1)
- Implement user prompt logic
- Run:
pytest tests/unit/test_bastion_default_behavior.py::TestUserPromptBehavior -v - All prompt tests should pass
Phase 3: Provisioning (Week 2)
- Modify
VMProvisionerto integrate bastion - Run:
pytest tests/unit/test_bastion_default_behavior.py::TestVMProvisioningWithBastion -v - All provisioning tests should pass
Phase 4: Integration (Week 2)
- Integrate all modules
- Run:
pytest tests/integration/test_bastion_default_integration.py -v - All integration tests should pass
Phase 5: E2E (Week 3)
- Complete end-to-end flows
- Run:
pytest tests/e2e/test_bastion_default_e2e.py -v - All E2E tests should pass
Phase 6: Security (Week 3)
- Security review and testing
- Run:
pytest tests/ -v -k security - All security tests should pass
Target Metrics:
- Test Coverage: >90%
- Test Pass Rate: 100%
- Test Execution Time: <5 minutes (unit + integration)
- E2E Execution Time: <20 minutes (with mocks)
Feature is complete when:
- All tests written (TDD approach)
- All unit tests pass (100%)
- All integration tests pass (100%)
- All E2E tests pass (100%)
- Code coverage >90%
- Security tests pass (100%)
- Performance criteria met
- Documentation updated
- Code reviewed and approved
Related Documents:
- BASTION_SECURITY_REQUIREMENTS.md
- BASTION_SECURITY_TESTING.md
- Issue #237: Make Bastion Host Usage Default
Test Files:
- Unit Tests:
tests/unit/test_bastion_default_behavior.py - Integration Tests:
tests/integration/test_bastion_default_integration.py - E2E Tests:
tests/e2e/test_bastion_default_e2e.py
Testing Resources:
Document Status: READY FOR IMPLEMENTATION Next Action: Begin Phase 1 - Detection Implementation Estimated Completion: 3 weeks from start date
END OF TEST SPECIFICATION