---
name: prowler-test-api
description: >
  Testing patterns for Prowler API: JSON:API, Celery tasks, RLS isolation, RBAC.
  Trigger: When writing tests for api/ (JSON:API requests/assertions, cross-tenant isolation, RBAC, Celery tasks, viewsets/serializers).
license: Apache-2.0
metadata:
  author: prowler-cloud
  version: "1.1.0"
  scope: [root, api]
  auto_invoke:
    - "Writing Prowler API tests"
    - "Testing RLS tenant isolation"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---

## Critical Rules

- ALWAYS use `response.json()["data"]` not `response.data`
- ALWAYS use `content_type = "application/vnd.api+json"` for PATCH/PUT requests
- ALWAYS use `format="vnd.api+json"` for POST requests
- ALWAYS test cross-tenant isolation - RLS returns 404, NOT 403
- NEVER skip RLS isolation tests when adding new endpoints
- NEVER use realistic-looking API keys in tests (TruffleHog will flag them)
- ALWAYS mock BOTH `.delay()` AND `Task.objects.get` for async task tests

---

## 1. Fixture Dependency Chain

```text
create_test_user (session) ─► tenants_fixture (function) ─► authenticated_client
                                     │
                                     └─► aws_provider ─► scans_fixture ─► findings_fixture
```

### Key Fixtures

| Fixture | Description |
|---------|-------------|
| `create_test_user` | Session user (`dev@prowler.com`) |
| `tenants_fixture` | 3 tenants: [0],[1] have membership, [2] isolated |
| `authenticated_client` | Django test client with JWT for tenant[0] |
| `authenticated_client_for_tenant_factory` | Creates a Django test client with JWT for a specific user and tenant |
| `provider_factory` | Creates one validated provider with provider-specific defaults |
| `aws_provider` | 1 AWS provider in tenant[0] |
| `aws_provider_pair` | 2 AWS providers in tenant[0] |
| `all_provider_types_fixture` | 1 provider for every supported provider type |
| `tasks_fixture` | 2 Celery tasks with TaskResult |

### RBAC Fixtures

| Fixture | Permissions |
|---------|-------------|
| `authenticated_client_rbac` | All permissions (admin) |
| `authenticated_client_rbac_noroles` | Membership but NO roles |
| `authenticated_client_no_permissions_rbac` | All permissions = False |

Use `authenticated_client` for normal view behavior tests. It uses a cheap JWT
and still runs the real request authentication path. Use serializer-generated
JWTs or API-key clients only when the test is specifically about token
obtain/refresh, invalid tokens, expired tokens, tenant switching by token, API
keys, or unauthenticated 401 behavior. Use
`authenticated_client_for_tenant_factory` when a test needs a cheap JWT client
for a different user or tenant.

---

## 2. JSON:API Requests

### POST (Create)
```python
response = client.post(
    reverse("provider-list"),
    data={"data": {"type": "providers", "attributes": {...}}},
    format="vnd.api+json",  # NOT content_type!
)
```

### PATCH (Update)
```python
response = client.patch(
    reverse("provider-detail", kwargs={"pk": provider.id}),
    data={"data": {"type": "providers", "id": str(provider.id), "attributes": {...}}},
    content_type="application/vnd.api+json",  # NOT format!
)
```

### Reading Responses
```python
data = response.json()["data"]
attrs = data["attributes"]
errors = response.json()["errors"]  # For 400 responses
```

---

## 3. RLS Isolation (Cross-Tenant)

**RLS returns 404, NOT 403** - the resource is invisible, not forbidden.

```python
def test_cross_tenant_access_denied(self, authenticated_client, tenants_fixture):
    other_tenant = tenants_fixture[2]  # Isolated tenant
    foreign_provider = Provider.objects.create(tenant_id=other_tenant.id, ...)

    response = authenticated_client.get(reverse("provider-detail", args=[foreign_provider.id]))
    assert response.status_code == status.HTTP_404_NOT_FOUND  # NOT 403!
```

---

## 4. Celery Task Testing

### Testing Strategies

| Strategy | Use For |
|----------|---------|
| Mock `.delay()` + `Task.objects.get` | Testing views that trigger tasks |
| `task.apply()` | Synchronous task logic testing |
| Mock `chain`/`group` | Testing Canvas orchestration |
| Mock `connection` | Testing `@set_tenant` decorator |
| Mock `apply_async` | Testing Beat scheduled tasks |

### Why NOT `task_always_eager`

| Problem | Impact |
|---------|--------|
| No task serialization | Misses argument type errors |
| No broker interaction | Hides connection issues |
| Different execution context | `self.request` behaves differently |

**Instead, use:** `task.apply()` for sync execution, mocking for isolation.

> **Full examples:** See [assets/api_test.py](assets/api_test.py) for `TestCeleryTaskLogic`, `TestCeleryCanvas`, `TestSetTenantDecorator`, `TestBeatScheduling`.

---

## 5. Fake Secrets (TruffleHog)

```python
# BAD - TruffleHog flags these:
api_key = "sk-test1234567890T3BlbkFJtest1234567890"

# GOOD - obviously fake:
api_key = "sk-fake-test-key-for-unit-testing-only"
```

---

## 6. Response Status Codes

| Scenario | Code |
|----------|------|
| Successful GET | 200 |
| Successful POST | 201 |
| Async operation (DELETE/scan trigger) | 202 |
| Sync DELETE | 204 |
| Validation error | 400 |
| Missing permission (RBAC) | 403 |
| RLS isolation / not found | 404 |

---

## Commands

```bash
cd api && uv run pytest -x --tb=short
cd api && uv run pytest -k "test_provider"
cd api && uv run pytest api/src/backend/api/tests/test_rbac.py
```

---

## Resources

- **Full Examples**: See [assets/api_test.py](assets/api_test.py) for complete test patterns
- **Fixture Reference**: See [references/test-api-docs.md](references/test-api-docs.md)
- **Fixture Source**: `api/src/backend/conftest.py`
