# Mock AWX/AAP Testing Guide

This guide shows you how to test AWX/Ansible Automation Platform API integrations **without a real AWX server** using mocked HTTP responses.

## Why Mock AWX Testing?

- **No Infrastructure Required**: Test without deploying AWX/Tower
- **Fast Execution**: Tests run in milliseconds
- **Reliable**: No network dependencies or authentication complexity
- **Controlled Scenarios**: Test edge cases, errors, and specific API responses
- **CI/CD Friendly**: Run in any environment without AWX infrastructure

## Quick Start

### Run All Mock Tests

```bash
# Run all AWX mock API tests
poetry run pytest tests/integration/test_awx_mock.py -v

# Run with specific test class
poetry run pytest tests/integration/test_awx_mock.py::TestAWXMockJobTemplates -v

# Run single test
poetry run pytest tests/integration/test_awx_mock.py::TestAWXMockJobs::test_get_job_status -v
```

### Example Output

```
tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_list_job_templates PASSED
tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_create_job_template PASSED
tests/integration/test_awx_mock.py::TestAWXMockJobTemplates::test_launch_job_template PASSED
tests/integration/test_awx_mock.py::TestAWXMockInventories::test_list_inventories PASSED
...
======================== 20 passed in 1.2s ========================
```

## AWX API Coverage

The mock tests cover all major AWX/AAP API endpoints:

### [YES] Job Templates
- List job templates
- Create job template
- Launch job template
- Get job template details

### [YES] Inventories
- List inventories
- Sync inventory sources
- Get inventory hosts and groups

### [YES] Projects
- List projects
- Update project (SCM sync)
- Get project details

### [YES] Credentials
- List credentials
- Create credentials
- Manage credential types

### [YES] Workflows
- List workflow templates
- Launch workflow
- Get workflow job status

### [YES] Jobs & Monitoring
- Get job status
- Get job output/stdout
- Cancel running jobs
- List job events

### [YES] Authentication & Errors
- 401 Unauthorized responses
- 403 Forbidden responses
- 404 Not Found responses
- 400 Validation errors

### [YES] Organizations
- List organizations
- Get organization details

## How It Works

Mock AWX testing uses the **`responses`** library to intercept HTTP requests:

```python
import responses
import requests

@responses.activate
def test_awx_job_template():
    # Mock AWX API response
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/",
        json={
            "count": 1,
            "results": [
                {
                    "id": 1,
                    "name": "Deploy Apache",
                    "playbook": "apache_deploy.yml"
                }
            ]
        },
        status=200
    )

    # Make real HTTP request (intercepted by responses)
    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/",
        headers={"Authorization": "Bearer test-token"}
    )

    # Verify response
    assert response.status_code == 200
    assert response.json()["count"] == 1
```

## Writing Your Own AWX Mock Tests

### Basic Structure

```python
import responses
import requests

@responses.activate
def test_my_awx_feature():
    # 1. Setup mock AWX response
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={
            "job": 42,
            "status": "pending"
        },
        status=201
    )

    # 2. Make API call
    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"extra_vars": {"env": "prod"}},
        headers={"Authorization": "Bearer test-token"}
    )

    # 3. Assert results
    assert response.status_code == 201
    assert response.json()["job"] == 42
```

### Testing Job Launches

```python
@responses.activate
def test_launch_and_monitor_job():
    # Mock job launch
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"job": 42, "status": "pending"},
        status=201
    )

    # Mock job status check
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/jobs/42/",
        json={"id": 42, "status": "successful", "failed": False},
        status=200
    )

    # Launch job
    launch_response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        headers={"Authorization": "Bearer test-token"}
    )
    job_id = launch_response.json()["job"]

    # Check job status
    status_response = requests.get(
        f"https://awx.example.com/api/v2/jobs/{job_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status_response.json()["status"] == "successful"
```

### Testing Workflows

```python
@responses.activate
def test_workflow_execution():
    # Mock workflow launch
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/workflow_job_templates/1/launch/",
        json={"workflow_job": 25, "status": "pending"},
        status=201
    )

    # Mock workflow status
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/workflow_jobs/25/",
        json={
            "id": 25,
            "status": "successful",
            "workflow_nodes": [
                {"job": 42, "status": "successful"},
                {"job": 43, "status": "successful"}
            ]
        },
        status=200
    )

    # Launch workflow
    response = requests.post(
        "https://awx.example.com/api/v2/workflow_job_templates/1/launch/",
        headers={"Authorization": "Bearer test-token"}
    )

    workflow_job_id = response.json()["workflow_job"]

    # Check workflow status
    status = requests.get(
        f"https://awx.example.com/api/v2/workflow_jobs/{workflow_job_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status.json()["status"] == "successful"
```

### Testing Inventory Updates

```python
@responses.activate
def test_inventory_sync():
    # Mock inventory source sync
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventory_sources/5/update/",
        json={"inventory_update": 15, "status": "pending"},
        status=202
    )

    # Mock inventory update status
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/inventory_updates/15/",
        json={"id": 15, "status": "successful", "total_hosts": 50},
        status=200
    )

    # Trigger sync
    sync_response = requests.post(
        "https://awx.example.com/api/v2/inventory_sources/5/update/",
        headers={"Authorization": "Bearer test-token"}
    )

    update_id = sync_response.json()["inventory_update"]

    # Check sync status
    status = requests.get(
        f"https://awx.example.com/api/v2/inventory_updates/{update_id}/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert status.json()["total_hosts"] == 50
```

## Common AWX API Patterns

### List Resources (GET with Pagination)

```python
responses.add(
    responses.GET,
    "https://awx.example.com/api/v2/job_templates/",
    json={
        "count": 100,
        "next": "/api/v2/job_templates/?page=2",
        "previous": None,
        "results": [
            {"id": 1, "name": "Template 1"},
            {"id": 2, "name": "Template 2"}
        ]
    },
    status=200
)
```

### Create Resource (POST)

```python
responses.add(
    responses.POST,
    "https://awx.example.com/api/v2/job_templates/",
    json={
        "id": 3,
        "name": "New Template",
        "created": "2026-02-16T12:00:00Z"
    },
    status=201
)
```

### Update Resource (PATCH/PUT)

```python
responses.add(
    responses.PATCH,
    "https://awx.example.com/api/v2/job_templates/1/",
    json={
        "id": 1,
        "name": "Updated Template",
        "modified": "2026-02-16T12:00:00Z"
    },
    status=200
)
```

### Delete Resource (DELETE)

```python
responses.add(
    responses.DELETE,
    "https://awx.example.com/api/v2/job_templates/1/",
    status=204  # No content
)
```

### Authentication Errors

```python
responses.add(
    responses.GET,
    "https://awx.example.com/api/v2/job_templates/",
    json={"detail": "Authentication credentials were not provided."},
    status=401
)
```

## AWX API Authentication Methods

AWX supports multiple authentication methods:

### Bearer Token (Recommended for testing)

```python
headers = {"Authorization": "Bearer test-token"}
```

### Basic Auth

```python
import base64

auth = base64.b64encode(b"username:password").decode()
headers = {"Authorization": f"Basic {auth}"}
```

### Session Auth (OAuth2)

```python
# Mock OAuth token endpoint
responses.add(
    responses.POST,
    "https://awx.example.com/api/o/token/",
    json={"access_token": "test-token", "expires_in": 36000},
    status=200
)
```

## Testing Error Scenarios

### Validation Errors

```python
@responses.activate
def test_invalid_job_template():
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/",
        json={
            "name": ["This field is required."],
            "inventory": ["Invalid inventory ID."]
        },
        status=400
    )

    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/",
        json={"description": "Missing required fields"},
        headers={"Authorization": "Bearer test-token"}
    )

    assert response.status_code == 400
    assert "name" in response.json()
```

### Permission Errors

```python
@responses.activate
def test_insufficient_permissions():
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        json={"detail": "You do not have permission to perform this action."},
        status=403
    )

    response = requests.post(
        "https://awx.example.com/api/v2/job_templates/1/launch/",
        headers={"Authorization": "Bearer limited-token"}
    )

    assert response.status_code == 403
```

### Resource Not Found

```python
@responses.activate
def test_template_not_found():
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/999/",
        json={"detail": "Not found."},
        status=404
    )

    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/999/",
        headers={"Authorization": "Bearer test-token"}
    )

    assert response.status_code == 404
```

## Debugging Mock Tests

### Inspect Requests Made

```python
@responses.activate
def test_with_debugging():
    responses.add(responses.GET, "https://awx.example.com/api/v2/jobs/42/", json={}, status=200)

    requests.get("https://awx.example.com/api/v2/jobs/42/", headers={"Authorization": "Bearer test"})

    # Debug: see what was sent
    print(f"Total requests: {len(responses.calls)}")
    print(f"URL: {responses.calls[0].request.url}")
    print(f"Headers: {dict(responses.calls[0].request.headers)}")
```

### Verify Query Parameters

```python
@responses.activate
def test_with_query_params():
    responses.add(
        responses.GET,
        "https://awx.example.com/api/v2/job_templates/",
        json={"results": []},
        status=200,
        match=[responses.matchers.query_param_matcher({"page": "2", "page_size": "20"})]
    )

    # This will only match if query params are exactly as specified
    response = requests.get(
        "https://awx.example.com/api/v2/job_templates/",
        params={"page": 2, "page_size": 20}
    )
```

## Performance

Mock AWX tests are extremely fast:

```bash
# 20 comprehensive AWX API tests in ~1.2 seconds
poetry run pytest tests/integration/test_awx_mock.py -v
# 20 passed in 1.20s
```

Compare to real AWX API calls which might take 1-3 seconds each due to:
- Network latency
- TLS handshake
- Authentication
- Database queries
- Job scheduling

## CI/CD Integration

<Mock tests work perfectly in CI/CD:

```yaml
# .github/workflows/test.yml
- name: Run AWX Mock Tests
  run: poetry run pytest tests/integration/test_awx_mock.py -v
```

No AWX infrastructure needed!

## Combining with Chef Server Mocks

Test complete migration scenarios by mocking both Chef Server AND AWX:

```python
@responses.activate
def test_chef_to_awx_migration():
    # Mock Chef Server node query
    responses.add(
        responses.GET,
        "https://chef.example.com/organizations/default/search/node",
        json={"rows": [{"name": "web-01", "platform": "ubuntu"}]},
        status=200
    )

    # Mock AWX inventory creation
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventories/",
        json={"id": 5, "name": "Chef Migrated Hosts"},
        status=201
    )

    # Mock AWX host creation
    responses.add(
        responses.POST,
        "https://awx.example.com/api/v2/inventories/5/hosts/",
        json={"id": 10, "name": "web-01"},
        status=201
    )

    # Your migration code here...
    # 1. Query Chef Server for nodes
    # 2. Create AWX inventory
    # 3. Populate AWX inventory with nodes
```

## Real AWX Testing

For testing with a real AWX instance, see:
- [AWX Official Docs](https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html)
- [AWX CLI](https://github.com/ansible/awx/tree/devel/awxkit)
- [AWX API Guide](https://docs.ansible.com/ansible-tower/latest/html/towerapi/index.html)

## Further Reading

- **AWX API Documentation**: https://docs.ansible.com/ansible-tower/latest/html/towerapi/
- **responses library**: https://github.com/getsentry/responses
- **Mock tests**: `tests/integration/test_awx_mock.py` (repository source)
- **Chef Server mocks**: [MOCK_CHEF_SERVER.md](MOCK_CHEF_SERVER.md)
