Skip to content
Open
Show file tree
Hide file tree
Changes from all 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