Skip to content

Commit 6261c37

Browse files
committed
CI testing
1 parent d1b57e5 commit 6261c37

20 files changed

Lines changed: 288 additions & 20 deletions

.github/workflows/ci.yml

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
env:
10+
POETRY_VERSION: "2.3.0"
11+
POETRY_VIRTUALENVS_IN_PROJECT: true
12+
13+
jobs:
14+
lint:
15+
name: Lint
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.12"
25+
26+
- name: Load cached Poetry installation
27+
id: cached-poetry
28+
uses: actions/cache@v4
29+
with:
30+
path: ~/.local
31+
key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }}
32+
33+
- name: Install Poetry
34+
if: steps.cached-poetry.outputs.cache-hit != 'true'
35+
uses: snok/install-poetry@v1
36+
with:
37+
version: ${{ env.POETRY_VERSION }}
38+
virtualenvs-create: true
39+
virtualenvs-in-project: true
40+
41+
- name: Load cached venv
42+
id: cached-venv
43+
uses: actions/cache@v4
44+
with:
45+
path: .venv
46+
key: venv-lint-${{ runner.os }}-py3.12-${{ hashFiles('poetry.lock') }}
47+
restore-keys: |
48+
venv-lint-${{ runner.os }}-py3.12-
49+
50+
- name: Install dependencies
51+
if: steps.cached-venv.outputs.cache-hit != 'true'
52+
run: poetry install --only dev --no-interaction
53+
54+
- name: Run Black formatter check
55+
run: poetry run black --check .
56+
57+
- name: Run isort import check
58+
run: poetry run isort --check-only .
59+
60+
- name: Run Flake8 linter
61+
run: poetry run flake8 .
62+
63+
test:
64+
name: Test
65+
runs-on: ubuntu-latest
66+
steps:
67+
- name: Checkout code
68+
uses: actions/checkout@v4
69+
70+
- name: Set up Python 3.12
71+
uses: actions/setup-python@v5
72+
with:
73+
python-version: "3.12"
74+
75+
- name: Load cached Poetry installation
76+
id: cached-poetry
77+
uses: actions/cache@v4
78+
with:
79+
path: ~/.local
80+
key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }}
81+
82+
- name: Install Poetry
83+
if: steps.cached-poetry.outputs.cache-hit != 'true'
84+
uses: snok/install-poetry@v1
85+
with:
86+
version: ${{ env.POETRY_VERSION }}
87+
virtualenvs-create: true
88+
virtualenvs-in-project: true
89+
90+
- name: Load cached venv
91+
id: cached-venv
92+
uses: actions/cache@v4
93+
with:
94+
path: .venv
95+
key: venv-test-${{ runner.os }}-py3.12-${{ hashFiles('poetry.lock') }}
96+
restore-keys: |
97+
venv-test-${{ runner.os }}-py3.12-
98+
99+
- name: Install dependencies
100+
if: steps.cached-venv.outputs.cache-hit != 'true'
101+
run: poetry install --no-interaction
102+
103+
- name: Run tests with coverage
104+
working-directory: src
105+
run: |
106+
poetry run pytest \
107+
--cov=. \
108+
--cov-report=xml \
109+
--cov-report=term-missing \
110+
-v \
111+
--tb=short
112+
env:
113+
DJANGO_ENV: testing
114+
ENVIRONMENT: TEST
115+
SECRET_KEY: test-secret-key-for-ci
116+
117+
- name: Upload coverage to Codecov
118+
uses: codecov/codecov-action@v5
119+
with:
120+
files: ./src/coverage.xml
121+
fail_ci_if_error: false
122+
verbose: true
123+
124+
security:
125+
name: Security Scan
126+
runs-on: ubuntu-latest
127+
steps:
128+
- name: Checkout code
129+
uses: actions/checkout@v4
130+
131+
- name: Set up Python 3.12
132+
uses: actions/setup-python@v5
133+
with:
134+
python-version: "3.12"
135+
136+
- name: Load cached Poetry installation
137+
id: cached-poetry
138+
uses: actions/cache@v4
139+
with:
140+
path: ~/.local
141+
key: poetry-${{ env.POETRY_VERSION }}-${{ runner.os }}
142+
143+
- name: Install Poetry
144+
if: steps.cached-poetry.outputs.cache-hit != 'true'
145+
uses: snok/install-poetry@v1
146+
with:
147+
version: ${{ env.POETRY_VERSION }}
148+
virtualenvs-create: true
149+
virtualenvs-in-project: true
150+
151+
- name: Load cached venv
152+
id: cached-venv
153+
uses: actions/cache@v4
154+
with:
155+
path: .venv
156+
key: venv-security-${{ runner.os }}-py3.12-${{ hashFiles('poetry.lock') }}
157+
restore-keys: |
158+
venv-security-${{ runner.os }}-py3.12-
159+
160+
- name: Install dependencies
161+
if: steps.cached-venv.outputs.cache-hit != 'true'
162+
run: poetry install --no-interaction
163+
164+
- name: Run Bandit security linter
165+
run: poetry run bandit -r . --skip B101 -f json -o bandit-report.json || true
166+
167+
- name: Display Bandit results
168+
run: poetry run bandit -r . --skip B101 -f txt || true
169+
170+
# Final status check for branch protection
171+
ci-success:
172+
name: CI Success
173+
needs: [lint, test, security]
174+
runs-on: ubuntu-latest
175+
if: always()
176+
steps:
177+
- name: Check all jobs passed
178+
run: |
179+
if [[ "${{ needs.lint.result }}" != "success" ]]; then
180+
echo "Lint job failed"
181+
exit 1
182+
fi
183+
if [[ "${{ needs.test.result }}" != "success" ]]; then
184+
echo "Test job failed"
185+
exit 1
186+
fi
187+
# Security is informational, doesn't fail CI
188+
echo "All required jobs passed!"

poetry.lock

Lines changed: 51 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ pytest-django = "^4.8" # Keep
5151
pytest-env = "^1.1" # Keep
5252
pytest-mock = "^3.14" # Keep
5353
responses = "^0.25" # Keep
54+
ruff = "^0.14.14"
55+
pytest-cov = "^7.0.0"
5456

5557
[tool.isort]
5658
line_length = 88
@@ -59,6 +61,22 @@ include_trailing_comma = true
5961
force_grid_wrap = 0
6062
use_parentheses = true
6163

64+
[tool.ruff]
65+
line-length = 88
66+
exclude = [
67+
"*/migrations/*",
68+
".venv",
69+
"__pycache__",
70+
]
71+
72+
[tool.ruff.lint]
73+
select = ["E", "F", "W", "I"]
74+
ignore = ["E501"] # Line too long (handled by formatter)
75+
76+
[tool.ruff.format]
77+
quote-style = "double"
78+
indent-style = "space"
79+
6280
[build-system]
6381
requires = ["poetry>=2.3.0"]
6482
build-backend = "poetry.masonry.api"

src/core/hashers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Custom password hashers with tuned parameters for web authentication.
33
"""
4+
45
from django.contrib.auth.hashers import Argon2PasswordHasher
56

67

@@ -21,6 +22,7 @@ class TunedArgon2PasswordHasher(Argon2PasswordHasher):
2122
- OWASP Password Storage Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
2223
- Django default uses 100 MB which is optimized for maximum security but too slow for web
2324
"""
25+
2426
time_cost = 2
2527
memory_cost = 19456 # 19 MB (OWASP minimum recommendation)
26-
parallelism = 1 # Single-threaded for web servers
28+
parallelism = 1 # Single-threaded for web servers

src/core/serializers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.contrib.auth import get_user_model
21
from dj_rest_auth.registration.serializers import (
32
RegisterSerializer as BaseRegisterSerializer,
43
)
@@ -7,6 +6,7 @@
76
PasswordResetConfirmSerializer as BasePasswordResetConfirmSerializer,
87
)
98
from dj_rest_auth.serializers import UserDetailsSerializer as BaseUserDetailsSerializer
9+
from django.contrib.auth import get_user_model
1010
from rest_framework import serializers
1111

1212
from core.models import Profile
@@ -78,9 +78,12 @@ def validate(self, data):
7878
data["username"] = data.get("email", "")
7979
# Check for duplicate email - this prevents hitting DB unique constraint
8080
from django.contrib.auth import get_user_model
81+
8182
User = get_user_model()
8283
if User.objects.filter(email=data.get("email")).exists():
83-
raise serializers.ValidationError({"email": ["A user with that email already exists."]})
84+
raise serializers.ValidationError(
85+
{"email": ["A user with that email already exists."]}
86+
)
8487
return data
8588

8689

src/core/tasks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.contrib.auth.models import User as AuthUser
66
from django.core.mail import send_mail
77
from django.template.loader import render_to_string
8-
from django_q.tasks import async_task
98
from mailchimp3 import MailChimp
109

1110
logger = logging.getLogger(__name__)

src/core/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from django.urls import include, path
2-
from django.views.generic import TemplateView
31
from dj_rest_auth.registration.views import VerifyEmailView
42
from dj_rest_auth.views import PasswordChangeView, PasswordResetConfirmView
3+
from django.urls import include, path
4+
from django.views.generic import TemplateView
55
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
66

77
from . import views
@@ -13,6 +13,7 @@ class DummyPasswordResetConfirmView(TemplateView):
1313
is handled by the PasswordResetConfirmView via POST.
1414
This pattern exists to satisfy Django's reverse() call in password reset emails.
1515
"""
16+
1617
template_name = ""
1718

1819

src/core/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
from dj_rest_auth.registration.views import RegisterView as BaseRegisterView
12
from django.contrib.auth.models import User
23
from django.utils.decorators import method_decorator
34
from django.views.decorators.debug import sensitive_post_parameters
45
from drf_yasg.openapi import IN_QUERY, TYPE_STRING, Parameter
56
from drf_yasg.utils import swagger_auto_schema
6-
from dj_rest_auth.registration.views import RegisterView as BaseRegisterView
77
from rest_framework.exceptions import NotFound, ValidationError
88
from rest_framework.generics import RetrieveUpdateAPIView
99
from rest_framework.permissions import IsAuthenticated

src/gunicorn_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,8 @@ def worker_int(worker):
212212
worker.log.info("worker received INT or QUIT signal")
213213

214214
# get traceback info
215-
import threading
216215
import sys
216+
import threading
217217
import traceback
218218

219219
id2name = {th.ident: th.name for th in threading.enumerate()}

src/settings/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
To change settings file:
66
`DJANGO_ENV=production python manage.py runserver`
77
"""
8+
89
from os import environ
910

1011
from split_settings.tools import include, optional

0 commit comments

Comments
 (0)