Skip to content

Commit

Permalink
Merge pull request #38 from brent-is-still-here/unit_tests
Browse files Browse the repository at this point in the history
Unit tests
  • Loading branch information
brent-is-still-here authored Nov 14, 2024
2 parents b59ba94 + af9d7cb commit f2623de
Show file tree
Hide file tree
Showing 4 changed files with 531 additions and 3 deletions.
51 changes: 51 additions & 0 deletions .github/workflows/unit_tests.yml
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
24 changes: 24 additions & 0 deletions config/settings/test.py
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'
141 changes: 140 additions & 1 deletion users/tests/test_security.py
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):
Expand Down Expand Up @@ -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"))

Loading

0 comments on commit f2623de

Please sign in to comment.