Skip to content

test: Add integration tests with Mock NITRO API for Issue #12#13

Open
slauger wants to merge 4 commits into
masterfrom
feature/mock-nitro-api
Open

test: Add integration tests with Mock NITRO API for Issue #12#13
slauger wants to merge 4 commits into
masterfrom
feature/mock-nitro-api

Conversation

@slauger
Copy link
Copy Markdown
Owner

@slauger slauger commented Dec 3, 2025

Summary

This PR adds comprehensive integration tests with a fully functional Mock NITRO API server, enabling end-to-end testing without requiring a real NetScaler appliance.

Motivation

Related to #12 - Chain certificate rotation tracking

To implement and test the chain rotation feature (#12), we need:

  1. A way to test the complete plugin workflow without NetScaler hardware
  2. Accurate simulation of NITRO API behavior, especially certificate serial number tracking
  3. Integration tests covering real-world scenarios including chain rotation

What's New

Integration Tests (tests/test_integration.py)

Six comprehensive end-to-end tests covering:

  • Initial Installation - Fresh NetScaler setup with chain and main certificate
  • Certificate Renewal - Renewal with same chain (serial number comparison)
  • Idempotent Runs - No-op when certificates are already up-to-date
  • ⏸️ Chain Rotation (Issue further automation? #12) - E6 → E7 rotation (test ready, waiting for implementation)
  • Authentication Errors - Proper error handling for invalid credentials
  • Custom Chain Names - Override auto-detection with --chain parameter

Enhanced Mock NITRO API

The mock server has been significantly improved:

New Capabilities:

  • Stores uploaded certificate files in memory (Base64-encoded)
  • Automatically extracts serial numbers from PEM certificates using PyOpenSSL
  • Tracks certificate serials for renewal detection
  • Simulates NetScaler behavior accurately

Key Implementation (tests/mock_nitro/state.py):

def extract_serial_from_cert(cert_path: str) -> Optional[str]:
    """Extract serial number from an uploaded certificate file."""
    filename = cert_path.split('/')[-1]
    cert_pem = base64.b64decode(_uploaded_files[filename])
    cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
    return hex(cert.get_serial_number())

This enables the mock to behave like a real NetScaler, automatically extracting and storing serial numbers when certificates are uploaded and installed.

Chain Rotation Test (Issue #12)

The test test_chain_rotation_issue_12 demonstrates the expected workflow:

# 1. Initial state: Main cert linked to E6
assert main_cert.get('linkcertkeyname') == 'E6'

# 2. Let's Encrypt rotates: chain.pem now contains E7
# (test simulates this by replacing the chain file)

# 3. Run hook with --update-chain --no-domain-check
run_hook('example.com', temp_certs, update_chain=True, no_domain_check=True)

# 4. Expected result: Main cert now linked to E7
assert main_cert.get('linkcertkeyname') == 'E7'

This test is currently skipped (not implemented yet), but provides a clear specification for the chain rotation feature.

Test Results

All existing tests still pass:

======================== test session starts =========================
tests/test_certificate_cn.py ............... [ 60%]   (15 passed)
tests/test_chain_rotation.py ....           [ 76%]   ( 4 passed)
tests/test_integration.py .....s            [100%]   ( 5 passed, 1 skipped)

================== 24 passed, 1 skipped in 8.87s ====================

Technical Details

Certificate Serial Number Tracking:
The mock API now accurately simulates NetScaler's serial number handling:

  1. When a certificate file is uploaded via systemfile endpoint, it's stored
  2. When a certificate object is created via sslcertkey endpoint, the serial is extracted from the uploaded file
  3. When querying certificates, the serial is returned (enabling renewal detection)

Fixtures Used:

  • mock_server - Flask server running in background thread
  • temp_certs - Real X.509 certificates generated with PyOpenSSL
  • env_vars - Environment variables for NITRO API connection

Next Steps

This PR provides the testing infrastructure for implementing #12. Once merged, the chain rotation feature can be developed with confidence, knowing that comprehensive tests are in place.

Checklist

  • All new tests pass
  • No existing tests were broken
  • Mock NITRO API accurately simulates NetScaler behavior
  • Tests cover realistic scenarios (installation, renewal, rotation)
  • Chain rotation test is ready (skipped until feature is implemented)
  • Documentation updated (Mock API README)

🤖 Generated with Claude Code

slauger and others added 4 commits December 3, 2025 22:57
Implemented a Flask-based mock of the NetScaler NITRO API to enable
testing without requiring a real NetScaler appliance.

Features:
- NITRO authentication with X-NITRO-USER/PASS headers
- Certificate management endpoints (add, update, get)
- Chain certificate linking/unlinking
- Proper error codes and messages matching real NetScaler
- Issue #12 simulation: Prevents linking to multiple chains

Files added:
- tests/mock_nitro/server.py: Flask server with all endpoints
- tests/mock_nitro/state.py: In-memory state management
- tests/mock_nitro/README.md: Documentation and usage
- tests/test_chain_rotation.py: Tests for Issue #12 scenario
- requirements-dev.txt: Development dependencies

All tests passing (4/4):
- test_chain_rotation_without_unlink_fails ✓
- test_chain_rotation_with_unlink_succeeds ✓
- test_link_is_idempotent ✓
- test_authentication_required ✓

This enables local testing and CI/CD without NetScaler hardware.
…cking

This commit adds end-to-end integration tests that run the complete plugin
against the Mock NITRO API server to verify real-world scenarios.

Changes:
- Add tests/test_integration.py with 6 integration tests covering:
  * Initial certificate installation (fresh NetScaler)
  * Certificate renewal with same chain
  * Idempotent runs (no changes needed)
  * Chain certificate rotation (E6 → E7) for Issue #12
  * Authentication error handling
  * Custom chain name override

- Enhanced Mock NITRO API (tests/mock_nitro/):
  * Store uploaded certificate files in memory
  * Automatically extract serial numbers from uploaded PEM certificates
  * Serial number tracking for renewal detection
  * Realistic certificate state management

The mock server now parses uploaded certificate files using PyOpenSSL to
extract serial numbers, simulating NetScaler's behavior. This enables
accurate testing of certificate renewal scenarios where the plugin
compares serial numbers to detect changes.

Test for Issue #12 (chain rotation) is included but skipped until the
feature is implemented. It demonstrates the expected behavior:
1. Detect main cert is linked to old chain (E6)
2. Unlink from old chain
3. Install new chain certificate (E7)
4. Link to new chain

All tests pass: 24 passed, 1 skipped

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The new integration tests require pytest and other dev dependencies
(Flask, requests, etc.) which are defined in requirements-dev.txt.

Changes:
- Install requirements-dev.txt in addition to requirements.txt
- Use 'pytest tests/ -v' instead of 'unittest discover'
- This enables pytest-based tests (test_chain_rotation.py, test_integration.py)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously, tests used fixed ports (5555, 5556) which caused 'Address already
in use' exceptions when Flask server threads didn't clean up fast enough
between tests. This resulted in noisy test output with many thread exceptions.

Changes:
- Mock server fixtures now find a free port dynamically using socket.bind()
- Each test gets its own unique port, preventing conflicts
- Test output is now clean with no OSError exceptions

Test results: 24 passed, 1 skipped, 1 warning (down from 9 warnings)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant