Skip to content

Commit 14fac2e

Browse files
authored
test(suite): set up initial testsuite (#36)
* test(suite): set up initial testsuite
1 parent 691e3e9 commit 14fac2e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+3599
-1392
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
CS_TOKEN="secret_token"
1+
CS_TOKEN=your-api-token-here
2+
# CS_BASE_URL=https://codesphere.com/api
3+
# CS_TEST_TEAM_ID=12345
4+
# CS_TEST_DC_ID=2

.github/copilot-instructions.md

Lines changed: 50 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,53 @@
1-
# Copilot Instructions for Codesphere Python SDK
1+
# Codesphere SDK - AI Instructions
22

3-
## Project Context
4-
5-
This repository contains the **Codesphere Python SDK**, an official asynchronous Python client for the [Codesphere Public API](https://codesphere.com/api/swagger-ui/). It provides a high-level, type-safe interface for managing Codesphere resources including Teams, Workspaces, Domains, Environment Variables, and Metadata.
6-
7-
The SDK follows a resource-based architecture where API operations are defined declaratively and executed through a centralized handler system.
3+
## Architecture & Core
4+
- **Async-First:** All I/O operations MUST be `async/await`.
5+
- **Base Classes:**
6+
- Models: Use `core.base.CamelModel` (handles camelCase API conversion).
7+
- Resources: Inherit from `ResourceBase` + `_APIOperationExecutor`.
8+
- **HTTP:** Use `APIHttpClient` (wraps httpx). Raise `httpx.HTTPStatusError` for API errors.
89

910
## Project Structure
10-
11-
```
12-
src/codesphere/
13-
├── __init__.py # Public API exports
14-
├── client.py # Main SDK entry point (CodesphereSDK)
15-
├── config.py # Settings via pydantic-settings
16-
├── exceptions.py # Custom exception classes
17-
├── http_client.py # Async HTTP client wrapper (APIHttpClient)
18-
├── utils.py # Utility functions
19-
├── core/ # Core SDK infrastructure
20-
│ ├── base.py # Base classes (ResourceBase, CamelModel, ResourceList)
21-
│ ├── handler.py # API operation executor (_APIOperationExecutor, APIRequestHandler)
22-
│ └── operations.py # APIOperation definition and AsyncCallable type
23-
└── resources/ # API resource implementations
24-
├── metadata/ # Datacenters, Plans, Images
25-
├── team/ # Teams and nested Domains
26-
│ └── domain/ # Domain management (schemas, operations, manager)
27-
└── workspace/ # Workspaces and nested resources
28-
├── envVars/ # Environment variables management
29-
├── landscape/ # (Placeholder)
30-
└── pipeline/ # (Placeholder)
31-
32-
tests/ # Test files mirroring src structure
33-
examples/ # Usage examples organized by resource type
34-
```
35-
36-
## Coding Guidelines
37-
38-
### General Principles
39-
40-
- **Async-First**: All API operations MUST be async. Use `async/await` syntax consistently.
41-
- **Type Hints**: Always provide complete type annotations for function parameters, return values, and class attributes.
42-
- **Pydantic Models**: Use Pydantic `BaseModel` for all data structures. Prefer `CamelModel` for API payloads to handle camelCase conversion.
43-
44-
### Naming Conventions
45-
46-
- **Variables/Functions**: Use `snake_case` (e.g., `workspace_id`, `list_datacenters`)
47-
- **Classes**: Use `PascalCase` (e.g., `WorkspaceCreate`, `APIHttpClient`)
48-
- **Constants/Operations**: Use `UPPER_SNAKE_CASE` with leading underscore for internal operations (e.g., `_LIST_BY_TEAM_OP`)
49-
- **Private Attributes**: Prefix with underscore (e.g., `_http_client`, `_token`)
50-
51-
### Resource Pattern
52-
53-
When adding new API resources, follow this structure:
54-
55-
1. **schemas.py**: Define Pydantic models for Create, Base, Update, and the main resource class
56-
2. **operations.py**: Define `APIOperation` instances for each endpoint
57-
3. **resources.py**: Create the resource class extending `ResourceBase` with operation fields
58-
4. **__init__.py**: Export public classes
59-
60-
```python
61-
# Example operation definition
62-
_GET_OP = APIOperation(
63-
method="GET",
64-
endpoint_template="/resources/{resource_id}",
65-
response_model=ResourceModel,
66-
)
67-
68-
# Example resource method
69-
async def get(self, resource_id: int) -> ResourceModel:
70-
return await self.get_op(data=resource_id)
71-
```
72-
73-
### Model Guidelines
74-
75-
- Extend `CamelModel` for API request/response models (automatic camelCase aliasing)
76-
- Extend `_APIOperationExecutor` for models that can perform operations on themselves
77-
- Use `Field(default=..., exclude=True)` for operation callables
78-
- Use `@cached_property` for lazy-loaded sub-managers (e.g., `workspace.env_vars`)
79-
80-
```python
81-
class Workspace(WorkspaceBase, _APIOperationExecutor):
82-
delete_op: AsyncCallable[None] = Field(default=_DELETE_OP, exclude=True)
83-
84-
async def delete(self) -> None:
85-
await self.delete_op()
86-
```
87-
88-
### Error Handling
89-
90-
- Raise `httpx.HTTPStatusError` for HTTP errors (handled by `APIHttpClient`)
91-
- Raise `RuntimeError` for SDK misuse (e.g., accessing resources without context manager)
92-
- Use custom exceptions from `exceptions.py` for SDK-specific errors
93-
94-
### Testing
95-
96-
- Use `pytest.mark.asyncio` for async tests
97-
- Use `@dataclass` for test case definitions with parametrization
98-
- Mock `httpx.AsyncClient` for HTTP request testing
99-
- Test files should mirror the source structure in `tests/`
100-
101-
### Code Style
102-
103-
- Line length: 88 characters (Ruff/Black standard)
104-
- Indentation: 4 spaces
105-
- Quotes: Double quotes for strings
106-
- Imports: Group stdlib, third-party, and local imports
107-
108-
### Development Commands
109-
110-
```bash
111-
make install # Set up development environment
112-
make lint # Run Ruff linter
113-
make format # Format code with Ruff
114-
make test # Run pytest
115-
make commit # Guided commit with Commitizen
116-
```
11+
- `src/codesphere/core/`: Base classes & handlers.
12+
- `src/codesphere/resources/`: Resource implementations (follow `schemas.py`, `operations.py`, `resources.py` pattern).
13+
- `tests/integration/`: Real API tests (require `CS_TOKEN`).
14+
- `tests/unit/`: Mocked logic tests.
15+
16+
## Resource Implementation Pattern
17+
When adding resources, strictly follow this pattern:
18+
19+
1. **`schemas.py`**: Define Pydantic models (inherit `CamelModel`).
20+
2. **`operations.py`**: Define `APIOperation` constants.
21+
```python
22+
_GET_OP = APIOperation(method="GET", endpoint_template="/res/{id}", response_model=ResModel)
23+
```
24+
3. **`resources.py`**: Implementation logic.
25+
```python
26+
class MyResource(ResourceBase, _APIOperationExecutor):
27+
# Operation callable (exclude from model dump)
28+
delete_op: AsyncCallable[None] = Field(default=_DELETE_OP, exclude=True)
29+
30+
async def delete(self) -> None:
31+
await self.delete_op()
32+
```
33+
34+
## Testing Rules
35+
- **Unit Tests (`tests/`):** MUST mock all HTTP calls (`unittest.mock.AsyncMock`).
36+
- **Integration Tests (`tests/integration/`):**
37+
- Use `pytest.mark.integration`.
38+
- Use provided fixtures: `sdk_client` (fresh client), `test_team_id`, `test_workspace`.
39+
- **Cleanup:** Always delete created resources in a `try...finally` block.
40+
41+
## Key Fixtures & Env Vars
42+
| Fixture | Scope | Description |
43+
|---|---|---|
44+
| `sdk_client` | func | Fresh `CodesphereSDK` instance. |
45+
| `test_team_id` | session | ID from `CS_TEST_TEAM_ID` (or default). |
46+
| `integration_token` | session | Token from `CS_TOKEN`. |
47+
48+
## Style & Naming
49+
- **Classes:** PascalCase (`WorkspaceCreate`).
50+
- **Vars:** snake_case.
51+
- **Internal Ops:** UPPER_SNAKE (`_LIST_OP`).
52+
- **Private:** Leading underscore (`_http_client`).
53+
- **Typing:** Strict type hints required.

.github/workflows/ci.yml

Lines changed: 120 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Python tests
1+
name: Python CI
22

33
on:
44
pull_request:
@@ -13,96 +13,129 @@ permissions:
1313
pull-requests: write
1414

1515
jobs:
16-
pytest:
16+
security_check:
17+
name: Security Check (Bandit)
1718
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
pull-requests: write
22+
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
27+
- name: Install uv package manager
28+
uses: astral-sh/setup-uv@v6
29+
with:
30+
activate-environment: true
31+
32+
- name: Install dependencies
33+
run: uv sync --extra dev
34+
shell: bash
35+
36+
- name: Run Bandit security check
37+
id: bandit_check
38+
run: |
39+
echo "Running Bandit security check..."
40+
set +e
41+
uv run bandit -r src/codesphere --format=custom --msg-template "{abspath}:{line}: {test_id}[{severity}]: {msg}" -o bandit-results.txt
42+
BANDIT_EXIT_CODE=$?
43+
set -e
44+
45+
echo "Bandit scan finished. Exit code: $BANDIT_EXIT_CODE"
46+
47+
# Zeige Ergebnisse im Log an
48+
if [ -f bandit-results.txt ]; then
49+
cat bandit-results.txt
50+
fi
51+
52+
echo "BANDIT_EXIT_CODE=${BANDIT_EXIT_CODE}" >> $GITHUB_ENV
53+
shell: bash
54+
55+
- name: Prepare Bandit comment body
56+
id: prep_bandit_comment
57+
if: github.event_name == 'pull_request'
58+
run: |
59+
echo "Preparing Bandit comment body..."
60+
COMMENT_BODY_FILE="bandit-comment-body.md"
61+
echo "COMMENT_BODY_FILE=${COMMENT_BODY_FILE}" >> $GITHUB_ENV
62+
63+
echo "### 🛡️ Bandit Security Scan Results" > $COMMENT_BODY_FILE
64+
echo "" >> $COMMENT_BODY_FILE
65+
66+
# WICHTIG: Hier wurde der Pfad korrigiert (das 'backend/' Prefix entfernt)
67+
if [ -s bandit-results.txt ]; then
68+
echo "\`\`\`text" >> $COMMENT_BODY_FILE
69+
cat bandit-results.txt >> $COMMENT_BODY_FILE
70+
echo "\`\`\`" >> $COMMENT_BODY_FILE
71+
else
72+
echo "✅ No security issues found by Bandit." >> $COMMENT_BODY_FILE
73+
fi
74+
shell: bash
75+
76+
- name: Find Comment
77+
uses: peter-evans/find-comment@v3
78+
id: fc
79+
with:
80+
issue-number: ${{ github.event.pull_request.number }}
81+
comment-author: 'github-actions[bot]'
82+
body-includes: Bandit Security Scan Results
83+
84+
- name: Post Bandit results as PR comment
85+
if: github.event_name == 'pull_request'
86+
uses: peter-evans/create-or-update-comment@v4
87+
with:
88+
token: ${{ secrets.GITHUB_TOKEN }}
89+
repository: ${{ github.repository }}
90+
issue-number: ${{ github.event.pull_request.number }}
91+
comment-id: ${{ steps.fc.outputs.comment-id }}
92+
body-file: ${{ env.COMMENT_BODY_FILE }}
93+
edit-mode: replace
94+
95+
- name: Fail if Bandit found issues
96+
if: env.BANDIT_EXIT_CODE != '0'
97+
run: exit ${{ env.BANDIT_EXIT_CODE }}
98+
99+
- name: Minimize uv cache
100+
run: uv cache prune --ci
18101

102+
pytest:
103+
name: Python Tests
104+
runs-on: ubuntu-latest
19105
permissions:
20106
contents: read
21107
pull-requests: write
108+
env:
109+
CS_TOKEN: 'dummy-token-for-ci'
22110

23111
steps:
24-
- name: Checkout repository
25-
uses: actions/checkout@v4
26-
27-
- name: Install uv package manager
28-
uses: astral-sh/setup-uv@v6
29-
with:
30-
activate-environment: true
31-
32-
- name: Install dependencies using uv
33-
run: |
34-
uv sync --extra dev
35-
shell: bash
36-
37-
- name: Run Bandit security check on backend code
38-
id: bandit_check
39-
run: |
40-
echo "Running Bandit security check..."
41-
set +e
42-
bandit -r . -c pyproject.toml --format=custom --msg-template "{abspath}:{line}: {test_id}[{severity}]: {msg}" -o bandit-results.txt
43-
cat bandit-results.txt
44-
BANDIT_EXIT_CODE=$?
45-
set -e
46-
echo "Bandit scan finished. Exit code: $BANDIT_EXIT_CODE"
47-
echo "BANDIT_EXIT_CODE=${BANDIT_EXIT_CODE}" >> $GITHUB_ENV
48-
shell: bash
49-
50-
- name: Prepare Bandit comment body
51-
id: prep_bandit_comment
52-
if: github.event_name == 'pull_request'
53-
run: |
54-
echo "Preparing Bandit comment body..."
55-
COMMENT_BODY_FILE="bandit-comment-body.md"
56-
echo "COMMENT_BODY_FILE=${COMMENT_BODY_FILE}" >> $GITHUB_ENV
57-
58-
echo "### 🛡️ Bandit Security Scan Results" > $COMMENT_BODY_FILE
59-
echo "" >> $COMMENT_BODY_FILE
60-
echo "" >> $COMMENT_BODY_FILE
61-
echo "" >> $COMMENT_BODY_FILE
62-
63-
if [ -s backend/bandit-results.txt ]; then
64-
echo "\`\`\`text" >> $COMMENT_BODY_FILE
65-
cat backend/bandit-results.txt >> $COMMENT_BODY_FILE
66-
echo "\`\`\`" >> $COMMENT_BODY_FILE
67-
else
68-
echo "✅ No security issues found by Bandit." >> $COMMENT_BODY_FILE
69-
fi
70-
shell: bash
71-
72-
- name: Find Comment
73-
uses: peter-evans/find-comment@v3
74-
id: fc
75-
with:
76-
issue-number: ${{ github.event.pull_request.number }}
77-
comment-author: 'github-actions[bot]'
78-
body-includes: Bandit Security Scan Results
79-
80-
- name: Post Bandit results as PR comment
81-
if: github.event_name == 'pull_request' && always()
82-
uses: peter-evans/create-or-update-comment@v4
83-
with:
84-
token: ${{ secrets.GITHUB_TOKEN }}
85-
repository: ${{ github.repository }}
86-
issue-number: ${{ github.event.pull_request.number }}
87-
comment-id: ${{ steps.fc.outputs.comment-id }}
88-
body-file: ${{ env.COMMENT_BODY_FILE }}
89-
edit-mode: replace
90-
91-
- name: Run tests with pytest using uv
92-
run: |
93-
pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. | tee pytest-coverage.txt
94-
shell: bash
95-
96-
- name: Pytest coverage comment
97-
if: github.event_name == 'pull_request' && always()
98-
uses: MishaKav/pytest-coverage-comment@main
99-
with:
100-
unique-id-for-comment: coverage-report
101-
pytest-xml-coverage-path: coverage.xml
102-
pytest-coverage-path: pytest-coverage.txt
103-
junitxml-path: junit/test-results.xml
104-
title: Pytest Coverage Report
105-
junitxml-title: Test Execution Summary
106-
107-
- name: Minimize uv cache
108-
run: uv cache prune --ci
112+
- name: Checkout repository
113+
uses: actions/checkout@v4
114+
115+
- name: Install uv package manager
116+
uses: astral-sh/setup-uv@v6
117+
with:
118+
activate-environment: true
119+
120+
- name: Install dependencies
121+
run: uv sync --extra dev
122+
shell: bash
123+
124+
- name: Run tests with pytest
125+
run: |
126+
uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. --ignore=tests/integration | tee pytest-coverage.txt
127+
shell: bash
128+
129+
- name: Pytest coverage comment
130+
if: github.event_name == 'pull_request' && always()
131+
uses: MishaKav/pytest-coverage-comment@main
132+
with:
133+
unique-id-for-comment: coverage-report
134+
pytest-xml-coverage-path: coverage.xml
135+
pytest-coverage-path: pytest-coverage.txt
136+
junitxml-path: junit/test-results.xml
137+
title: Pytest Coverage Report
138+
junitxml-title: Test Execution Summary
139+
140+
- name: Minimize uv cache
141+
run: uv cache prune --ci

0 commit comments

Comments
 (0)