Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
# Design Document: PAT Renewal
|
||||
|
||||
## Overview
|
||||
|
||||
The PAT Renewal system automates Personal Access Token lifecycle management across GitLab, Jira, Confluence, and OrgMyLife services. It runs as a GitHub Actions scheduled workflow (weekly, Mondays at 06:00 UTC) that:
|
||||
|
||||
1. Reads a PAT Registry (JSON) tracking all managed tokens
|
||||
2. Checks each token's expiry status via service-specific API calls
|
||||
3. Auto-rotates GitLab PATs using the GitLab Personal Access Tokens API
|
||||
4. Creates OrgMyLife tasks or sends email alerts for tokens requiring manual renewal
|
||||
5. Produces a summary report in the GitHub Actions run log
|
||||
|
||||
The system integrates with the existing GitHub Actions infrastructure already used in the workspace (deploy workflows, OrgMyLife sync) and leverages the OrgMyLife API for task creation alerts.
|
||||
|
||||
**Key Design Decisions:**
|
||||
- **Python-based workflow script**: Matches the workspace's primary language (Python 3.11+) and allows reuse of patterns from the AI-Orchestrator project (httpx for HTTP, pytest for testing)
|
||||
- **JSON registry file**: Simple, version-controllable, human-readable format stored in the repository
|
||||
- **GitHub Actions secrets for token storage**: Leverages existing secure secret management without introducing new infrastructure
|
||||
- **Idempotent alerting**: Prevents duplicate OrgMyLife tasks by checking for existing open tasks before creating new ones
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "GitHub Actions"
|
||||
CRON[Cron Trigger<br/>Monday 06:00 UTC] --> WF[pat-check Workflow]
|
||||
DISPATCH[workflow_dispatch] --> WF
|
||||
WF --> SCRIPT[pat_manager.py]
|
||||
end
|
||||
|
||||
subgraph "PAT Manager Script"
|
||||
SCRIPT --> LOAD[Load PAT Registry]
|
||||
LOAD --> CHECK[Expiry Checker]
|
||||
CHECK --> CLASSIFY{Classify PATs}
|
||||
CLASSIFY -->|expiring soon + auto| ROTATE[GitLab Rotator]
|
||||
CLASSIFY -->|expiring soon + manual| ALERT[Alert Sender]
|
||||
CLASSIFY -->|expired| ALERT
|
||||
CLASSIFY -->|healthy| REPORT[Summary Reporter]
|
||||
ROTATE --> SECRET_UPDATE[Update GitHub Secret]
|
||||
ROTATE -->|failure| ALERT
|
||||
SECRET_UPDATE --> REPORT
|
||||
ALERT --> REPORT
|
||||
end
|
||||
|
||||
subgraph "External Services"
|
||||
CHECK --> GITLAB_API[GitLab API]
|
||||
CHECK --> JIRA_API[Jira API]
|
||||
CHECK --> CONFLUENCE_API[Confluence API]
|
||||
CHECK --> ORGMYLIFE_API[OrgMyLife API]
|
||||
ROTATE --> GITLAB_API
|
||||
SECRET_UPDATE --> GH_API[GitHub API]
|
||||
ALERT --> ORGMYLIFE_API
|
||||
ALERT --> EMAIL[Email / SMTP]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
LOAD --> REGISTRY[pat-registry.json]
|
||||
REPORT --> LOG[Workflow Run Log]
|
||||
end
|
||||
```
|
||||
|
||||
### Component Interaction Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GHA as GitHub Actions
|
||||
participant PM as pat_manager.py
|
||||
participant REG as pat-registry.json
|
||||
participant GL as GitLab API
|
||||
participant JIRA as Jira API
|
||||
participant OML as OrgMyLife API
|
||||
participant GH as GitHub API
|
||||
|
||||
GHA->>PM: Trigger (cron/dispatch)
|
||||
PM->>REG: Load registry
|
||||
REG-->>PM: PAT entries
|
||||
|
||||
loop For each PAT
|
||||
alt GitLab PAT
|
||||
PM->>GL: GET /personal_access_tokens
|
||||
GL-->>PM: expires_at
|
||||
else Jira/Confluence/OrgMyLife PAT
|
||||
PM->>JIRA: Authenticated API call
|
||||
JIRA-->>PM: 200 OK / 401 Unauthorized
|
||||
end
|
||||
PM->>PM: Classify (healthy/expiring/expired/check failed)
|
||||
end
|
||||
|
||||
alt GitLab PAT expiring soon
|
||||
PM->>GL: POST /personal_access_tokens/{id}/rotate
|
||||
GL-->>PM: New token + expiry
|
||||
PM->>GH: Update secret
|
||||
PM->>REG: Update expiry_date
|
||||
end
|
||||
|
||||
alt Manual token expiring/expired
|
||||
PM->>OML: POST /api/tasks (create alert task)
|
||||
end
|
||||
|
||||
PM->>GHA: Summary report (log output)
|
||||
```
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### 1. PAT Registry Module (`registry.py`)
|
||||
|
||||
Responsible for loading, validating, and persisting the PAT registry JSON file.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PatEntry:
|
||||
service: str # "GitLab" | "Jira" | "Confluence" | "OrgMyLife"
|
||||
token_name: str # max 128 chars
|
||||
expiry_date: str | None # ISO 8601 (YYYY-MM-DD) or None
|
||||
renewal_method: str # "auto" | "manual"
|
||||
|
||||
class PatRegistry:
|
||||
def load(self, path: Path) -> list[PatEntry]: ...
|
||||
def validate_entry(self, entry: dict) -> PatEntry | ValidationError: ...
|
||||
def save(self, path: Path, entries: list[PatEntry]) -> None: ...
|
||||
def create_empty(self, path: Path) -> None: ...
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- `service` must be one of: `GitLab`, `Jira`, `Confluence`, `OrgMyLife`
|
||||
- `token_name` must be non-empty, max 128 characters
|
||||
- `expiry_date` must be valid ISO 8601 (YYYY-MM-DD) or null
|
||||
- `renewal_method` must match service: GitLab → "auto", others → "manual"
|
||||
- No duplicate `(service, token_name)` combinations
|
||||
|
||||
### 2. Expiry Checker Module (`checker.py`)
|
||||
|
||||
Queries each service API to determine PAT status.
|
||||
|
||||
```python
|
||||
class PatStatus(Enum):
|
||||
HEALTHY = "healthy"
|
||||
EXPIRING_SOON = "expiring soon"
|
||||
EXPIRED = "expired"
|
||||
CHECK_FAILED = "check failed"
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
entry: PatEntry
|
||||
status: PatStatus
|
||||
days_remaining: int | None
|
||||
error_message: str | None
|
||||
|
||||
class ExpiryChecker:
|
||||
def __init__(self, expiry_window_days: int = 14, timeout: int = 30, max_retries: int = 2): ...
|
||||
async def check_all(self, entries: list[PatEntry], secrets: dict[str, str]) -> list[CheckResult]: ...
|
||||
async def check_gitlab(self, entry: PatEntry, token: str) -> CheckResult: ...
|
||||
async def check_jira(self, entry: PatEntry, token: str) -> CheckResult: ...
|
||||
async def check_confluence(self, entry: PatEntry, token: str) -> CheckResult: ...
|
||||
async def check_orgmylife(self, entry: PatEntry, token: str) -> CheckResult: ...
|
||||
```
|
||||
|
||||
**Service-Specific Logic:**
|
||||
- **GitLab**: `GET /personal_access_tokens` → read `expires_at` field, compare with window
|
||||
- **Jira**: Authenticated call → HTTP 401 = expired
|
||||
- **Confluence**: Authenticated call → HTTP 401 = expired
|
||||
- **OrgMyLife**: Authenticated call → HTTP 401/403 = expired
|
||||
|
||||
### 3. GitLab Rotator Module (`rotator.py`)
|
||||
|
||||
Handles automatic token rotation for GitLab PATs.
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class RotationResult:
|
||||
success: bool
|
||||
new_expiry_date: str | None
|
||||
error_message: str | None
|
||||
|
||||
class GitLabRotator:
|
||||
def __init__(self, timeout: int = 30): ...
|
||||
async def rotate(self, token_id: str, current_token: str) -> RotationResult: ...
|
||||
```
|
||||
|
||||
### 4. Secret Updater Module (`secret_updater.py`)
|
||||
|
||||
Updates GitHub Actions secrets with new token values.
|
||||
|
||||
```python
|
||||
class SecretUpdater:
|
||||
def __init__(self, github_token: str, repo_owner: str, repo_name: str): ...
|
||||
async def update_secret(self, secret_name: str, secret_value: str) -> bool: ...
|
||||
```
|
||||
|
||||
### 5. Alert Sender Module (`alerter.py`)
|
||||
|
||||
Sends alerts via OrgMyLife task creation or email.
|
||||
|
||||
```python
|
||||
class AlertChannel(Enum):
|
||||
ORGMYLIFE = "orgmylife"
|
||||
EMAIL = "email"
|
||||
|
||||
@dataclass
|
||||
class AlertResult:
|
||||
sent: bool
|
||||
channel: AlertChannel
|
||||
error_message: str | None
|
||||
|
||||
class AlertSender:
|
||||
def __init__(self, channel: AlertChannel, config: dict): ...
|
||||
async def send_alert(self, result: CheckResult) -> AlertResult: ...
|
||||
async def check_existing_task(self, service: str, token_name: str, expiry_date: str) -> bool: ...
|
||||
```
|
||||
|
||||
**OrgMyLife Integration:**
|
||||
- Uses `POST /api/tasks` with `X-API-Key` header for authentication
|
||||
- Task title format: `[PAT Renewal] {service} - {token_name}`
|
||||
- Includes days remaining and renewal instructions in description
|
||||
- Checks for existing open tasks before creating duplicates
|
||||
|
||||
### 6. Summary Reporter Module (`reporter.py`)
|
||||
|
||||
Produces the workflow summary log.
|
||||
|
||||
```python
|
||||
class SummaryReporter:
|
||||
def report(self, results: list[CheckResult], rotations: list[RotationResult]) -> str: ...
|
||||
```
|
||||
|
||||
### 7. Main Orchestrator (`pat_manager.py`)
|
||||
|
||||
Entry point that coordinates all modules.
|
||||
|
||||
```python
|
||||
async def main() -> int:
|
||||
"""Returns exit code: 0 for success, 1 for failures."""
|
||||
...
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### PAT Registry JSON Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": { "type": "string", "const": "1.0" },
|
||||
"tokens": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"service": {
|
||||
"type": "string",
|
||||
"enum": ["GitLab", "Jira", "Confluence", "OrgMyLife"]
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string",
|
||||
"maxLength": 128,
|
||||
"minLength": 1
|
||||
},
|
||||
"expiry_date": {
|
||||
"type": ["string", "null"],
|
||||
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
|
||||
},
|
||||
"renewal_method": {
|
||||
"type": "string",
|
||||
"enum": ["auto", "manual"]
|
||||
}
|
||||
},
|
||||
"required": ["service", "token_name", "renewal_method"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["version", "tokens"]
|
||||
}
|
||||
```
|
||||
|
||||
### Example PAT Registry
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"tokens": [
|
||||
{
|
||||
"service": "GitLab",
|
||||
"token_name": "ci-pipeline-token",
|
||||
"expiry_date": "2025-08-15",
|
||||
"renewal_method": "auto"
|
||||
},
|
||||
{
|
||||
"service": "Jira",
|
||||
"token_name": "jira-api-access",
|
||||
"expiry_date": "2025-07-20",
|
||||
"renewal_method": "manual"
|
||||
},
|
||||
{
|
||||
"service": "Confluence",
|
||||
"token_name": "confluence-bot",
|
||||
"expiry_date": "2025-09-01",
|
||||
"renewal_method": "manual"
|
||||
},
|
||||
{
|
||||
"service": "OrgMyLife",
|
||||
"token_name": "orchestrator-api-key",
|
||||
"expiry_date": null,
|
||||
"renewal_method": "manual"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Check Result Data Model
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
entry: PatEntry
|
||||
status: PatStatus # healthy | expiring soon | expired | check failed
|
||||
days_remaining: int | None # None if check failed or no expiry date
|
||||
error_message: str | None # Populated only for check_failed
|
||||
stale_warning: bool = False # True if API succeeds but stored date has passed
|
||||
```
|
||||
|
||||
### GitHub Actions Workflow Configuration
|
||||
|
||||
```yaml
|
||||
name: PAT Lifecycle Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
check-pats:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install dependencies
|
||||
run: pip install httpx
|
||||
- name: Run PAT Manager
|
||||
env:
|
||||
GITLAB_PAT: ${{ secrets.GITLAB_PAT }}
|
||||
JIRA_PAT: ${{ secrets.JIRA_PAT }}
|
||||
CONFLUENCE_PAT: ${{ secrets.CONFLUENCE_PAT }}
|
||||
ORGMYLIFE_API_KEY: ${{ secrets.ORGMYLIFE_API_SECRET }}
|
||||
ORGMYLIFE_USER: ${{ secrets.ORGMYLIFE_USER }}
|
||||
ORGMYLIFE_PASS: ${{ secrets.ORGMYLIFE_PASS }}
|
||||
GH_TOKEN: ${{ secrets.PAT_MANAGER_TOKEN }}
|
||||
ALERT_CHANNEL: orgmylife
|
||||
run: python scripts/pat_manager.py
|
||||
```
|
||||
|
||||
### Environment Variables / Secrets Mapping
|
||||
|
||||
| Secret Name | Purpose | Used By |
|
||||
|---|---|---|
|
||||
| `GITLAB_PAT` | GitLab API access + rotation source | Checker, Rotator |
|
||||
| `JIRA_PAT` | Jira authenticated check | Checker |
|
||||
| `CONFLUENCE_PAT` | Confluence authenticated check | Checker |
|
||||
| `ORGMYLIFE_API_SECRET` | OrgMyLife API key for task creation | Alerter |
|
||||
| `ORGMYLIFE_USER` | OrgMyLife basic auth username | Alerter |
|
||||
| `ORGMYLIFE_PASS` | OrgMyLife basic auth password | Alerter |
|
||||
| `PAT_MANAGER_TOKEN` | GitHub API token for secret updates | SecretUpdater |
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||
|
||||
### Property 1: Registry serialization round-trip
|
||||
|
||||
*For any* valid list of PAT entries, serializing the registry to JSON and deserializing it back SHALL produce an equivalent list of entries with identical field values.
|
||||
|
||||
**Validates: Requirements 1.1**
|
||||
|
||||
### Property 2: Validation accepts only valid entries
|
||||
|
||||
*For any* dictionary representing a PAT entry, the validator SHALL accept it if and only if it contains all required fields (`service`, `token_name`, `expiry_date`, `renewal_method`), the `service` is one of the four supported values, the `token_name` is non-empty and at most 128 characters, the `expiry_date` is either null or a valid ISO 8601 date string, and the `renewal_method` matches the service (GitLab → "auto", others → "manual").
|
||||
|
||||
**Validates: Requirements 1.2, 1.4**
|
||||
|
||||
### Property 3: Invalid entries and duplicates preserve registry state
|
||||
|
||||
*For any* PAT registry and any entry that is either invalid (fails validation) or a duplicate (same service + token_name already exists), attempting to add that entry SHALL leave the registry unchanged — the entry count and all existing entries remain identical.
|
||||
|
||||
**Validates: Requirements 1.3, 1.5**
|
||||
|
||||
### Property 4: Expiry classification is deterministic and correct
|
||||
|
||||
*For any* PAT entry with an expiry date and any reference date, the classification SHALL be:
|
||||
- "expired" if `expiry_date < reference_date`
|
||||
- "expiring soon" if `reference_date <= expiry_date <= reference_date + expiry_window`
|
||||
- "healthy" if `expiry_date > reference_date + expiry_window`
|
||||
|
||||
This holds regardless of whether the expiry date comes from the service API or the stored registry value.
|
||||
|
||||
**Validates: Requirements 2.2, 2.3, 2.4, 6.5**
|
||||
|
||||
### Property 5: Classification completeness
|
||||
|
||||
*For any* PAT registry with N entries, after running the expiry check, the system SHALL produce exactly N classification results, each with exactly one status from the set {"healthy", "expiring soon", "expired", "check failed"}.
|
||||
|
||||
**Validates: Requirements 2.7**
|
||||
|
||||
### Property 6: Registry update after successful rotation
|
||||
|
||||
*For any* GitLab PAT entry and any valid rotation response containing a new expiry date, after successful rotation the registry entry's `expiry_date` SHALL equal the new expiry date from the rotation response.
|
||||
|
||||
**Validates: Requirements 3.3**
|
||||
|
||||
### Property 7: Alert triggering correctness
|
||||
|
||||
*For any* set of classified PATs:
|
||||
- If ALL PATs have status "healthy", THEN zero alerts SHALL be sent
|
||||
- For each manual-renewal PAT with status "expiring soon" or "expired", exactly one alert SHALL be triggered
|
||||
|
||||
**Validates: Requirements 4.1, 5.4**
|
||||
|
||||
### Property 8: Alert message completeness
|
||||
|
||||
*For any* PAT entry requiring an alert:
|
||||
- If the alert channel is OrgMyLife, the task title SHALL contain both the service name and token identifier
|
||||
- If the alert channel is email, the email body SHALL contain the service name, token identifier, expiration date, and a renewal instructions URL
|
||||
|
||||
**Validates: Requirements 4.2, 4.3**
|
||||
|
||||
### Property 9: Days remaining calculation
|
||||
|
||||
*For any* expiry date and reference date, the computed `days_remaining` SHALL equal `max(0, (expiry_date - reference_date).days)` — yielding 0 for already-expired tokens and the correct positive integer for future dates.
|
||||
|
||||
**Validates: Requirements 4.4**
|
||||
|
||||
### Property 10: Alert idempotence
|
||||
|
||||
*For any* PAT that requires an alert, if an open OrgMyLife task already exists for the same service, token_name, and expiry_date, THEN no new task SHALL be created. Running the alert flow twice for the same PAT state SHALL produce at most one task.
|
||||
|
||||
**Validates: Requirements 4.5**
|
||||
|
||||
### Property 11: Summary report completeness
|
||||
|
||||
*For any* list of check results, the generated summary report SHALL contain one line per result, and each line SHALL include the service name, token identifier, and classification status.
|
||||
|
||||
**Validates: Requirements 5.3**
|
||||
|
||||
### Property 12: Non-auth HTTP errors yield "check failed"
|
||||
|
||||
*For any* HTTP response status code that is not 200-299, 401, or 403, the classification SHALL be "check failed". This includes 4xx errors (other than 401/403), 5xx errors, and timeout conditions.
|
||||
|
||||
**Validates: Requirements 6.6**
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
| Operation | Max Retries | Backoff | Fallback |
|
||||
|---|---|---|---|
|
||||
| Service API health check | 2 | 5s linear | Classify as "check failed" |
|
||||
| GitLab token rotation | 1 | None | Manual alert flow |
|
||||
| GitHub secret update | 0 | N/A | Alert + non-zero exit |
|
||||
| OrgMyLife task creation | 1 | 2s | Log error + non-zero exit |
|
||||
| Email sending | 1 | 2s | Log error + non-zero exit |
|
||||
|
||||
### Error Classification
|
||||
|
||||
```python
|
||||
class PatManagerError(Exception):
|
||||
"""Base exception for PAT Manager errors."""
|
||||
pass
|
||||
|
||||
class RegistryValidationError(PatManagerError):
|
||||
"""Raised when a registry entry fails validation."""
|
||||
def __init__(self, entry: dict, failed_fields: list[str]): ...
|
||||
|
||||
class ServiceCheckError(PatManagerError):
|
||||
"""Raised when a service API check fails after retries."""
|
||||
def __init__(self, service: str, token_name: str, reason: str): ...
|
||||
|
||||
class RotationError(PatManagerError):
|
||||
"""Raised when GitLab token rotation fails."""
|
||||
def __init__(self, token_name: str, http_status: int | None, reason: str): ...
|
||||
|
||||
class SecretUpdateError(PatManagerError):
|
||||
"""Raised when GitHub secret update fails."""
|
||||
def __init__(self, secret_name: str, reason: str): ...
|
||||
|
||||
class AlertError(PatManagerError):
|
||||
"""Raised when alert delivery fails."""
|
||||
def __init__(self, channel: str, reason: str): ...
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| 0 | All checks passed, all actions succeeded |
|
||||
| 1 | One or more alerts could not be delivered |
|
||||
| 2 | Registry validation failed (malformed registry file) |
|
||||
|
||||
### Security Error Handling
|
||||
|
||||
- Token values are masked via `::add-mask::{value}` before any operation that might log output
|
||||
- On rotation failure, the old token remains valid — no service disruption
|
||||
- On secret update failure, the new token exists in GitLab but the workflow still uses the old one — alert is critical
|
||||
- All HTTP errors are logged without including request/response bodies that might contain tokens
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Property-Based Tests (Hypothesis)
|
||||
|
||||
The project uses Python 3.11+ with pytest. Property-based tests will use the **Hypothesis** library, which is the standard PBT framework for Python.
|
||||
|
||||
**Configuration:**
|
||||
- Minimum 100 examples per property test (`@settings(max_examples=100)`)
|
||||
- Each test tagged with a comment referencing the design property
|
||||
- Tag format: `# Feature: pat-renewal, Property {number}: {property_text}`
|
||||
|
||||
**Properties to implement:**
|
||||
1. Registry round-trip serialization
|
||||
2. Validation correctness (accepts valid, rejects invalid)
|
||||
3. Invalid/duplicate entries preserve state
|
||||
4. Expiry classification determinism
|
||||
5. Classification completeness
|
||||
6. Registry update after rotation
|
||||
7. Alert triggering correctness
|
||||
8. Alert message completeness
|
||||
9. Days remaining calculation
|
||||
10. Alert idempotence
|
||||
11. Summary report completeness
|
||||
12. Non-auth HTTP errors → "check failed"
|
||||
|
||||
**Generators needed:**
|
||||
- `pat_entries()` — generates valid PatEntry instances with random services, token names, dates
|
||||
- `invalid_entries()` — generates dictionaries missing fields or with invalid values
|
||||
- `expiry_dates(relative_to)` — generates dates in various ranges (past, within window, beyond window)
|
||||
- `http_status_codes()` — generates non-success, non-auth status codes
|
||||
- `check_results()` — generates lists of CheckResult with various statuses
|
||||
|
||||
### Unit Tests (pytest)
|
||||
|
||||
Example-based tests for specific scenarios:
|
||||
- Empty registry file creation (Req 1.6)
|
||||
- Successful rotation logging format (Req 3.6)
|
||||
- Expired GitLab PAT skips rotation (Req 3.7)
|
||||
- Stale expiry_date warning (Req 6.7)
|
||||
- Token masking with `::add-mask::` (Req 7.2)
|
||||
|
||||
### Integration Tests (pytest + httpx mock)
|
||||
|
||||
Mock-based tests for external service interactions:
|
||||
- GitLab API expiry check with mocked response (Req 6.1)
|
||||
- Jira/Confluence 401 interpretation (Req 6.2, 6.3)
|
||||
- OrgMyLife 401/403 interpretation (Req 6.4)
|
||||
- GitLab rotation API call + retry on failure (Req 3.1, 3.4)
|
||||
- GitHub secret update after rotation (Req 3.2)
|
||||
- Rotation success + secret update failure → alert (Req 3.5)
|
||||
- Alert channel unreachable → non-zero exit (Req 4.6)
|
||||
- Service timeout after retries → "check failed" (Req 2.5)
|
||||
- Null expiry_date classification (Req 2.6, 6.8)
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
- Workflow YAML contains correct cron schedule (Req 5.1)
|
||||
- Workflow YAML contains workflow_dispatch trigger (Req 5.2)
|
||||
- All API URLs use HTTPS scheme (Req 7.3)
|
||||
- No token values written to non-secret locations (Req 7.1)
|
||||
|
||||
### Test Dependencies
|
||||
|
||||
```
|
||||
hypothesis>=6.100
|
||||
pytest>=8.0
|
||||
pytest-asyncio>=0.23
|
||||
pytest-cov>=5.0
|
||||
respx>=0.21 # httpx mock library
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user