<!-- Copyright (c) 2026 defconxt. All rights reserved. -->
<!-- Licensed under AGPL-3.0 — see LICENSE file for details. -->
---
name: building-security-unit-tests
description: >-
  Write targeted unit tests that verify security controls including input validation,
  authentication enforcement, authorization boundaries, cryptographic operations,
  and secure session management. Integrate into pytest and Jest test suites.
domain: cybersecurity
subdomain: devsecops
tags:
  - security-testing
  - unit-tests
  - pytest
  - jest
  - input-validation
  - authentication
  - authorization
  - shift-left
version: "1.0"
author: defconxt
license: AGPL-3.0
metadata:
  mitre-attack: []
---

# Building Security Unit Tests

## Overview

Security unit tests verify that security controls function correctly at the code level.
This skill covers writing tests for input validation, authentication, authorization,
cryptographic operations, and error handling. Tests act as regression guards ensuring
security controls are not accidentally weakened during refactoring.

Mode: `[MODE: BLUE]` — Shift-left security verification through automated testing.

## Prerequisites

| Requirement | Details |
|---|---|
| Python 3.10+ with pytest (`pip install pytest pytest-cov`) | Required |
| Node.js 18+ with Jest for JavaScript projects | Required |
| Test fixtures for authentication tokens, malicious inputs | Required |
| Code coverage tooling (coverage.py, istanbul) | Required |

## Key Concepts

### Input Validation Tests (pytest)

```python
# tests/security/test_input_validation.py
import pytest
from app.validators import sanitize_input, validate_email, validate_url

class TestSQLInjection:
    """Verify SQL injection payloads are rejected or neutralized."""

    SQL_PAYLOADS = [
        "' OR '1'='1",
        "'; DROP TABLE users; --",
        "1 UNION SELECT * FROM passwords",
        "admin'--",
        "1; EXEC xp_cmdshell('whoami')",
    ]

    @pytest.mark.parametrize("payload", SQL_PAYLOADS)
    def test_rejects_sql_injection(self, payload: str) -> None:
        result = sanitize_input(payload)
        assert "'" not in result or result != payload
        assert "--" not in result
        assert "UNION" not in result.upper()

class TestXSSPrevention:
    """Verify XSS payloads are sanitized."""

    XSS_PAYLOADS = [
        "<script>alert('xss')</script>",
        '<img src=x onerror="alert(1)">',
        "javascript:alert(document.cookie)",
        '<svg/onload=alert("xss")>',
    ]

    @pytest.mark.parametrize("payload", XSS_PAYLOADS)
    def test_rejects_xss_payload(self, payload: str) -> None:
        result = sanitize_input(payload)
        assert "<script" not in result.lower()
        assert "onerror" not in result.lower()
        assert "javascript:" not in result.lower()
```

### Authentication Tests (pytest)

```python
# tests/security/test_authentication.py
import pytest
from app.auth import authenticate, create_token, verify_token

class TestAuthentication:
    def test_rejects_empty_credentials(self) -> None:
        assert authenticate("", "") is None

    def test_rejects_wrong_password(self, valid_user) -> None:
        assert authenticate(valid_user.email, "wrong_password") is None

    def test_token_expires(self, valid_user) -> None:
        token = create_token(valid_user, expires_in=-1)
        with pytest.raises(TokenExpiredError):
            verify_token(token)

    def test_rejects_tampered_token(self, valid_user) -> None:
        token = create_token(valid_user)
        tampered = token[:-4] + "XXXX"
        with pytest.raises(InvalidTokenError):
            verify_token(tampered)

    def test_rejects_none_algorithm(self, valid_user) -> None:
        """CVE-2015-9235: JWT 'none' algorithm bypass."""
        import jwt
        payload = {"sub": valid_user.id, "alg": "none"}
        forged = jwt.encode(payload, key="", algorithm="HS256")
        with pytest.raises(InvalidTokenError):
            verify_token(forged)
```

### Authorization Boundary Tests (pytest)

```python
# tests/security/test_authorization.py
import pytest
from app.auth import create_token
from app.api import app

class TestAuthorizationBoundary:
    def test_user_cannot_access_admin_endpoint(self, regular_user) -> None:
        token = create_token(regular_user)
        response = app.test_client().get(
            "/admin/users",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 403

    def test_user_cannot_access_other_user_data(self, user_a, user_b) -> None:
        token = create_token(user_a)
        response = app.test_client().get(
            f"/api/users/{user_b.id}/profile",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert response.status_code == 403

    def test_unauthenticated_request_rejected(self) -> None:
        response = app.test_client().get("/api/users/me")
        assert response.status_code == 401
```

### Security Tests in Jest

```javascript
// tests/security/validation.test.js
const { sanitize, validateCSRFToken } = require('../../src/security');

describe('Input Sanitization', () => {
  const xssPayloads = [
    '<script>alert(1)</script>',
    '<img src=x onerror=alert(1)>',
    '"><script>alert(document.domain)</script>',
  ];

  test.each(xssPayloads)('neutralizes XSS payload: %s', (payload) => {
    const result = sanitize(payload);
    expect(result).not.toContain('<script');
    expect(result).not.toContain('onerror');
  });

  test('rejects missing CSRF token', async () => {
    const result = validateCSRFToken(undefined, 'session-id');
    expect(result.valid).toBe(false);
  });

  test('rejects mismatched CSRF token', async () => {
    const result = validateCSRFToken('wrong-token', 'session-id');
    expect(result.valid).toBe(false);
  });
});
```

### CI Integration

```yaml
name: Security Unit Tests
on:
  pull_request:

jobs:
  security-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install -r requirements-test.txt
      - name: Run security tests
        run: |
          pytest tests/security/ -v \
            --tb=short \
            --cov=app \
            --cov-report=xml \
            --junitxml=security-test-results.xml
      - name: Enforce coverage on security modules
        run: |
          coverage report --include="app/auth/*,app/validators/*" --fail-under=90
```

## Workflow

### Step 1: Generate Test Templates

```bash
node scripts/agent.js --action generate --source-dir ./src/ --output ./tests/security/
```

### Step 2: Analyze Security Test Coverage

```bash
node scripts/agent.js --action coverage --test-dir ./tests/security/ --output /tmp/security-coverage.json
```

### Step 3: List Untested Security Controls

```bash
node scripts/agent.js --action gaps --source-dir ./src/ --test-dir ./tests/security/ --output /tmp/gaps.json
```

## Verification

- [ ] Security unit tests run in CI on every PR
- [ ] Input validation tests cover OWASP Top 10 payloads
- [ ] Authentication tests verify token expiry, tampering, algorithm confusion
- [ ] Authorization tests verify IDOR and privilege escalation boundaries
- [ ] Security module code coverage ≥ 90%
- [ ] Test failures block merge

## References

- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
- [pytest Documentation](https://docs.pytest.org/)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
