Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
CS_TOKEN="secret_token"
# Required: Your Codesphere API token
# Get this from your Codesphere account settings
CS_TOKEN=your-api-token-here
# CS_BASE_URL=https://codesphere.com/api
# CS_TEST_TEAM_ID=12345
# CS_TEST_DC_ID=1
154 changes: 140 additions & 14 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ src/codesphere/
└── pipeline/ # (Placeholder)

tests/ # Test files mirroring src structure
├── conftest.py # Shared unit test fixtures
├── core/ # Core infrastructure tests
├── resources/ # Resource unit tests
└── integration/ # Integration tests (real API)
├── conftest.py # Integration fixtures & workspace setup
├── test_domains.py
├── test_env_vars.py
├── test_metadata.py
├── test_teams.py
└── test_workspaces.py

examples/ # Usage examples organized by resource type
```

Expand Down Expand Up @@ -67,7 +78,7 @@ _GET_OP = APIOperation(

# Example resource method
async def get(self, resource_id: int) -> ResourceModel:
return await self.get_op(data=resource_id)
return await self.get_op(resource_id=resource_id)
```

### Model Guidelines
Expand All @@ -91,26 +102,141 @@ class Workspace(WorkspaceBase, _APIOperationExecutor):
- Raise `RuntimeError` for SDK misuse (e.g., accessing resources without context manager)
- Use custom exceptions from `exceptions.py` for SDK-specific errors

### Testing

- Use `pytest.mark.asyncio` for async tests
- Use `@dataclass` for test case definitions with parametrization
- Mock `httpx.AsyncClient` for HTTP request testing
- Test files should mirror the source structure in `tests/`

### Code Style

- Line length: 88 characters (Ruff/Black standard)
- Indentation: 4 spaces
- Quotes: Double quotes for strings
- Imports: Group stdlib, third-party, and local imports

### Development Commands
---

## Testing Guidelines

When adding features or making changes, appropriate tests are **required**. The SDK uses two types of tests:

### Unit Tests

Located in `tests/` (excluding `tests/integration/`). These mock HTTP responses and test SDK logic in isolation.

**When to write unit tests:**
- New Pydantic models or schemas
- New API operations
- Core handler or utility logic changes

**Unit test patterns:**

```python
import pytest
from dataclasses import dataclass
from unittest.mock import AsyncMock, MagicMock

# Use @dataclass for parameterized test cases
@dataclass
class WorkspaceTestCase:
name: str
workspace_id: int
expected_name: str

@pytest.mark.asyncio
@pytest.mark.parametrize("case", [
WorkspaceTestCase(name="basic", workspace_id=123, expected_name="test-ws"),
])
async def test_workspace_get(case: WorkspaceTestCase):
"""Should fetch a workspace by ID."""
mock_response = MagicMock()
mock_response.json.return_value = {"id": case.workspace_id, "name": case.expected_name}

# Test implementation...
```

### Integration Tests

Located in `tests/integration/`. These run against the real Codesphere API.

**When to write integration tests:**
- New API endpoints (CRUD operations)
- Changes to request/response serialization
- Schema field changes (detect API contract changes early)

**Integration test patterns:**

```python
import pytest
from codesphere import CodesphereSDK

pytestmark = [pytest.mark.integration, pytest.mark.asyncio]

class TestMyResourceIntegration:
"""Integration tests for MyResource endpoints."""

async def test_list_resources(self, sdk_client: CodesphereSDK):
"""Should retrieve a list of resources."""
resources = await sdk_client.my_resource.list()

assert isinstance(resources, list)

async def test_create_and_delete(
self,
sdk_client: CodesphereSDK,
test_team_id: int,
):
"""Should create and delete a resource."""
resource = await sdk_client.my_resource.create(name="test")

try:
assert resource.name == "test"
finally:
# Always cleanup created resources
await resource.delete()
```

**Available integration test fixtures** (from `tests/integration/conftest.py`):

| Fixture | Scope | Description |
|---------|-------|-------------|
| `sdk_client` | function | Fresh SDK client for each test |
| `session_sdk_client` | session | Shared SDK client for setup/teardown |
| `test_team_id` | session | Team ID for testing |
| `test_workspace` | session | Single pre-created workspace |
| `test_workspaces` | session | List of 2 test workspaces |
| `integration_token` | session | The API token (from `CS_TOKEN`) |

**Environment variables:**

| Variable | Required | Description |
|----------|----------|-------------|
| `CS_TOKEN` | Yes | Codesphere API token |
| `CS_TEST_TEAM_ID` | No | Specific team ID (defaults to first team) |
| `CS_TEST_DC_ID` | No | Datacenter ID (defaults to 1) |

### Running Tests

```bash
make test # Run unit tests only
make test-unit # Run unit tests only (explicit)
make test-integration # Run integration tests (requires CS_TOKEN)
```

### Test Requirements Checklist

When submitting a PR, ensure:

- [ ] **New endpoints** have integration tests covering all operations
- [ ] **New models** have unit tests for serialization/deserialization
- [ ] **Bug fixes** include a test that reproduces the issue
- [ ] **All tests pass** locally before pushing

---

## Development Commands

```bash
make install # Set up development environment
make lint # Run Ruff linter
make format # Format code with Ruff
make test # Run pytest
make commit # Guided commit with Commitizen
make install # Set up development environment
make lint # Run Ruff linter
make format # Format code with Ruff
make test # Run unit tests
make test-unit # Run unit tests (excludes integration)
make test-integration # Run integration tests
make commit # Guided commit with Commitizen
```
91 changes: 91 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Integration Tests

on:
# Manual trigger with optional inputs
workflow_dispatch:
inputs:
test_team_id:
description: "Team ID to use for testing (optional)"
required: false
type: string
test_dc_id:
description: "Datacenter ID for test resources"
required: false
default: "1"
type: string

# Run on pull requests
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "src/codesphere/**"
- "tests/integration/**"
- ".github/workflows/integration.yml"

permissions:
contents: read

env:
PYTHON_VERSION: "3.12"

jobs:
integration-tests:
name: Run Integration Tests
runs-on: ubuntu-latest
# Only run if the secret is available (prevents failures on forks)
if: ${{ github.repository == 'Datata1/codesphere-python' || github.event_name == 'workflow_dispatch' }}

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv package manager
uses: astral-sh/setup-uv@v6
with:
activate-environment: true

- name: Install dependencies
run: uv sync --extra dev

- name: Run integration tests
env:
CS_TOKEN: ${{ secrets.CS_TOKEN }}
CS_TEST_TEAM_ID: ${{ inputs.test_team_id || secrets.CS_TEST_TEAM_ID }}
CS_TEST_DC_ID: ${{ inputs.test_dc_id || '1' }}
run: |
echo "Running integration tests..."
uv run pytest tests/integration -v --run-integration \
--junitxml=junit/integration-results.xml \
--tb=short

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-test-results
path: junit/integration-results.xml
retention-days: 30

- name: Minimize uv cache
run: uv cache prune --ci

integration-tests-summary:
name: Integration Tests Summary
runs-on: ubuntu-latest
needs: integration-tests
if: always()

steps:
- name: Download test results
uses: actions/download-artifact@v4
with:
name: integration-test-results
path: junit

- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: junit/integration-results.xml
check_name: Integration Test Results
comment_mode: off
Loading
Loading