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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ dropkit/_version.txt

# Virtual environments
.venv

# Git worktrees
.worktrees/
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dropkit/
### Username
- **Derived from DigitalOcean account email**, not configured
- Fetched via `/v2/account`, sanitized for Linux compatibility
- `john.doe@trailofbits.com` → `john_doe`
- `john.doe@example.com` → `john_doe`

### SSH Hostname
- All SSH entries use `dropkit.<droplet-name>` format
Expand Down
13 changes: 5 additions & 8 deletions dropkit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,19 @@ def get_username(self) -> str:
@staticmethod
def _sanitize_email_for_username(email: str) -> str:
"""
Sanitize email address to create a valid username.
Sanitize email address to create a valid Linux username.

Removes @trailofbits.com suffix and replaces special characters.
Extracts the local part (before @) and replaces special characters
with underscores. Ensures the result is a valid Linux username.

Args:
email: Email address from DigitalOcean account

Returns:
Sanitized username suitable for Linux user creation
"""
# Remove @trailofbits.com suffix (case insensitive)
username = re.sub(r"@trailofbits\.com$", "", email, flags=re.IGNORECASE)

# If no @trailofbits.com, just take the part before @
if "@" in username:
username = username.split("@")[0]
# Extract local part (before @)
username = email.split("@")[0]

# Replace dots, hyphens, and other special characters with underscores
username = re.sub(r"[^a-z0-9_]", "_", username.lower())
Expand Down
24 changes: 22 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
class TestDigitalOceanAPI:
"""Tests for DigitalOceanAPI class."""

def test_sanitize_email_for_username_trailofbits(self):
"""Test sanitizing trailofbits.com email."""
def test_sanitize_email_for_username_trailofbits_backwards_compat(self):
"""Test backwards compatibility: trailofbits.com emails still work."""
username = DigitalOceanAPI._sanitize_email_for_username("[email protected]")
assert username == "john_doe"

Expand All @@ -33,6 +33,26 @@ def test_sanitize_email_for_username_empty_fallback(self):
username = DigitalOceanAPI._sanitize_email_for_username("@trailofbits.com")
assert username == "user"

def test_sanitize_email_for_username_google(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add here the backtest for the @trailofbits.com usecase

"""Test sanitizing google.com email."""
username = DigitalOceanAPI._sanitize_email_for_username("[email protected]")
assert username == "jane_smith"

def test_sanitize_email_for_username_gmail(self):
"""Test sanitizing gmail.com email."""
username = DigitalOceanAPI._sanitize_email_for_username("[email protected]")
assert username == "user123"

def test_sanitize_email_for_username_corporate(self):
"""Test sanitizing corporate email with subdomain."""
username = DigitalOceanAPI._sanitize_email_for_username("[email protected]")
assert username == "dev_ops"

def test_sanitize_email_for_username_plus_addressing(self):
"""Test sanitizing email with plus addressing (any domain)."""
username = DigitalOceanAPI._sanitize_email_for_username("[email protected]")
assert username == "user_tag"


class TestValidatePositiveInt:
"""Tests for _validate_positive_int method."""
Expand Down