-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from brent-is-still-here/unit_tests
Unit tests
- Loading branch information
Showing
4 changed files
with
531 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
name: Django CI | ||
|
||
on: | ||
pull_request: | ||
branches: [ main ] | ||
push: | ||
branches: [ main ] | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
|
||
services: | ||
postgres: | ||
image: postgres:15 | ||
env: | ||
POSTGRES_USER: postgres | ||
POSTGRES_PASSWORD: postgres | ||
POSTGRES_DB: github_actions | ||
ports: | ||
- 5432:5432 | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval 10s | ||
--health-timeout 5s | ||
--health-retries 5 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.11' | ||
|
||
- name: Install Dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -r requirements.txt | ||
- name: Create Recovery Key | ||
run: | | ||
python -c "from cryptography.fernet import Fernet; key = Fernet.generate_key(); open('recovery_key.key', 'wb').write(key)" | ||
- name: Run Tests | ||
env: | ||
DATABASE_URL: postgres://postgres:postgres@localhost:5432/github_actions | ||
DJANGO_SETTINGS_MODULE: config.settings.test | ||
SECRET_KEY: your-test-secret-key-here | ||
run: | | ||
python manage.py test -v 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from .base import * | ||
|
||
DEBUG = False | ||
|
||
DATABASES = { | ||
'default': { | ||
'ENGINE': 'django.db.backends.postgresql', | ||
'NAME': 'github_actions', | ||
'USER': 'postgres', | ||
'PASSWORD': 'postgres', | ||
'HOST': 'localhost', | ||
'PORT': '5432', | ||
} | ||
} | ||
|
||
# Test specific settings | ||
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' | ||
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] | ||
|
||
# Disable any resource-intensive settings | ||
MAX_ACCOUNTS_PER_EMAIL = 2 # If you use this setting | ||
|
||
# Override any settings that require external services | ||
MAILTRAP_API_TOKEN = 'dummy-token-for-testing' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,11 @@ | ||
from django.test import Client, TestCase | ||
from django.urls import reverse | ||
from django.contrib.auth import get_user_model | ||
from users.models import HashedEmail, RecoveryKey | ||
from django.contrib.auth.hashers import check_password | ||
import hashlib | ||
|
||
User = get_user_model() | ||
|
||
class TestRegistrationSecurity(TestCase): | ||
def setUp(self): | ||
|
@@ -48,4 +54,137 @@ def test_xss_username(self): | |
'username', | ||
'Enter a valid username. This value may contain only letters, ' | ||
'numbers, and @/./+/-/_ characters.' | ||
) | ||
) | ||
|
||
class TestSecurityMeasures(TestCase): | ||
def setUp(self): | ||
self.client = Client() | ||
self.signup_url = reverse('users:signup') | ||
self.login_url = reverse('users:login') | ||
self.email = "[email protected]" | ||
self.password = "SecurePass123!" | ||
self.username = "testuser" | ||
|
||
# Create a test user | ||
self.user = User.objects.create_user( | ||
username=self.username, | ||
email=self.email, | ||
password=self.password | ||
) | ||
self.hashed_email = HashedEmail.get_or_create_hash(self.email) | ||
self.user.hashed_email = self.hashed_email | ||
self.user.save() | ||
|
||
def test_email_purging(self): | ||
"""Test that email is properly purged after recovery key is viewed""" | ||
# Start with a fresh user that hasn't viewed their recovery key | ||
self.user.recovery_key_viewed = False | ||
self.user.email_purged = False | ||
self.user.save() | ||
|
||
# Log in first | ||
self.client.force_login(self.user) | ||
|
||
# Simulate first login behavior by generating recovery key | ||
recovery_key = RecoveryKey.generate_recovery_key() | ||
recovery_key_obj = RecoveryKey(user=self.user) | ||
recovery_key_obj.encrypt_recovery_key(recovery_key) | ||
recovery_key_obj.save() | ||
|
||
# Set up session with recovery key | ||
session = self.client.session | ||
session['recovery_key'] = recovery_key | ||
session.save() | ||
|
||
# Now get the recovery key page | ||
response = self.client.get(reverse('users:show_recovery_key')) | ||
self.assertEqual(response.status_code, 302) | ||
|
||
def test_purged_email_recovery(self): | ||
"""Test that purged emails cannot be recovered""" | ||
# Purge the email | ||
self.user.email_purged = True | ||
self.user.email = '' | ||
self.user.save() | ||
|
||
# Try to find user by original email through various means | ||
self.assertFalse(User.objects.filter(email=self.email).exists()) | ||
|
||
# Verify we can still find user by hashed email | ||
email_hash = HashedEmail.hash_email(self.email) | ||
self.assertTrue(User.objects.filter(hashed_email__email_hash=email_hash).exists()) | ||
|
||
def test_hashed_email_privacy(self): | ||
"""Test that email hashes are one-way and can't be reversed""" | ||
email1 = "[email protected]" | ||
email2 = "[email protected]" | ||
|
||
hash1 = HashedEmail.hash_email(email1) | ||
hash2 = HashedEmail.hash_email(email2) | ||
|
||
# Verify different emails produce different hashes | ||
self.assertNotEqual(hash1, hash2) | ||
|
||
# Verify same email always produces same hash | ||
self.assertEqual(hash1, HashedEmail.hash_email(email1)) | ||
|
||
# Verify hash length and format (SHA-256 produces 64 character hex string) | ||
self.assertEqual(len(hash1), 64) | ||
self.assertTrue(all(c in '0123456789abcdef' for c in hash1)) | ||
|
||
def test_password_encryption(self): | ||
"""Test that passwords are properly hashed and not stored in plaintext""" | ||
# Verify password is not stored in plaintext | ||
self.assertNotEqual(self.user.password, self.password) | ||
|
||
# Verify password can be verified | ||
self.assertTrue(check_password(self.password, self.user.password)) | ||
|
||
# Verify incorrect password fails | ||
self.assertFalse(check_password("wrongpassword", self.user.password)) | ||
|
||
def test_sql_injection_prevention(self): | ||
"""Test that SQL injection attempts are prevented""" | ||
injection_attempts = [ | ||
"'; DROP TABLE users; --", | ||
"' OR '1'='1", | ||
"' UNION SELECT password FROM users; --", | ||
"admin'--", | ||
] | ||
|
||
for injection in injection_attempts: | ||
# Test login | ||
response = self.client.post(self.login_url, { | ||
'username': injection, | ||
'password': injection, | ||
}) | ||
self.assertEqual(response.status_code, 200) # Should stay on login page | ||
self.assertFalse(response.wsgi_request.user.is_authenticated) | ||
|
||
# Test signup | ||
response = self.client.post(self.signup_url, { | ||
'username': injection, | ||
'email': f'{injection}@example.com', | ||
'password1': 'SecurePass123!', | ||
'password2': 'SecurePass123!', | ||
}) | ||
self.assertEqual(response.status_code, 200) # Should stay on signup page | ||
self.assertFalse(User.objects.filter(username=injection).exists()) | ||
|
||
def test_recovery_key_encryption(self): | ||
"""Test that recovery keys are properly encrypted""" | ||
# Generate and encrypt recovery key | ||
plain_recovery_key = RecoveryKey.generate_recovery_key() | ||
recovery_key = RecoveryKey(user=self.user) | ||
recovery_key.encrypt_recovery_key(plain_recovery_key) | ||
recovery_key.save() | ||
|
||
# Verify encrypted key is not stored in plaintext | ||
self.assertNotEqual(recovery_key.encrypted_key, plain_recovery_key) | ||
|
||
# Verify key can be verified | ||
self.assertTrue(recovery_key.verify_recovery_key(plain_recovery_key)) | ||
|
||
# Verify incorrect key fails | ||
self.assertFalse(recovery_key.verify_recovery_key("wrongkey")) | ||
|
Oops, something went wrong.