feat: Add organization member support with memberType field
Implements support for both individual persons and organizations as members. Changes: - Added MemberType enum (INDIVIDUAL, ORGANIZATION) to distinguish member types - Added member_type column to database (defaults to INDIVIDUAL for backward compatibility) - Added company_name field for organizations - Made first_name nullable (required for individuals, optional for organizations) - Implemented conditional validation: - INDIVIDUAL members require first_name - ORGANIZATION members require company_name - Updated GraphQL schema with new memberType and companyName fields - Updated all resolvers to handle new fields and validation - Added comprehensive unit tests for validation logic - Updated existing tests to work with new fields - All 53 tests passing Technical notes: - Using memberType instead of 'kind' to avoid GraphQL introspection conflicts - Using native_enum=False for SQLite compatibility - Using batch_alter_table for SQLite ALTER COLUMN compatibility - Backward compatible: existing members automatically become INDIVIDUAL type 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -80,5 +80,5 @@ class TestMemberModel:
|
||||
repr_str = repr(member)
|
||||
assert "Member" in repr_str
|
||||
assert f"id={member.id}" in repr_str
|
||||
assert "first_name='David'" in repr_str
|
||||
assert "last_name='Smith'" in repr_str
|
||||
assert "type=INDIVIDUAL" in repr_str
|
||||
assert "name='David Smith'" in repr_str
|
||||
|
||||
@@ -86,7 +86,7 @@ class TestCreateMemberMutation:
|
||||
mutation = Mutation()
|
||||
input_data = CreateMemberInput(first_name="")
|
||||
|
||||
with pytest.raises(ValidationError, match="firstName cannot be empty"):
|
||||
with pytest.raises(ValidationError, match="firstName is required for individual members"):
|
||||
await mutation.create_member(input=input_data)
|
||||
|
||||
|
||||
|
||||
+105
-20
@@ -2,26 +2,12 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.validation import ValidationError, validate_email, validate_first_name, validate_phone
|
||||
|
||||
|
||||
class TestFirstNameValidation:
|
||||
"""Tests for validate_first_name function."""
|
||||
|
||||
def test_valid_first_name(self):
|
||||
"""Test that valid firstName passes validation."""
|
||||
validate_first_name("Alice")
|
||||
# No exception = pass
|
||||
|
||||
def test_empty_string_raises_error(self):
|
||||
"""Test that empty string raises ValidationError."""
|
||||
with pytest.raises(ValidationError, match="firstName cannot be empty"):
|
||||
validate_first_name("")
|
||||
|
||||
def test_whitespace_only_raises_error(self):
|
||||
"""Test that whitespace-only string raises ValidationError."""
|
||||
with pytest.raises(ValidationError, match="firstName cannot be empty"):
|
||||
validate_first_name(" ")
|
||||
from src.validation import (
|
||||
ValidationError,
|
||||
validate_email,
|
||||
validate_phone,
|
||||
validate_member_type_requirements,
|
||||
)
|
||||
|
||||
|
||||
class TestEmailValidation:
|
||||
@@ -110,3 +96,102 @@ class TestPhoneValidation:
|
||||
"""Test that empty string passes validation (optional field)."""
|
||||
validate_phone("")
|
||||
# No exception = pass
|
||||
|
||||
|
||||
class TestMemberTypeRequirements:
|
||||
"""Tests for validate_member_type_requirements function."""
|
||||
|
||||
def test_individual_with_first_name_valid(self):
|
||||
"""Test INDIVIDUAL with firstName passes."""
|
||||
validate_member_type_requirements(
|
||||
member_type="INDIVIDUAL",
|
||||
first_name="John",
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_individual_without_first_name_invalid(self):
|
||||
"""Test INDIVIDUAL without firstName fails."""
|
||||
with pytest.raises(ValidationError, match="firstName is required for individual members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="INDIVIDUAL",
|
||||
first_name=None,
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_individual_with_empty_first_name_invalid(self):
|
||||
"""Test INDIVIDUAL with empty firstName fails."""
|
||||
with pytest.raises(ValidationError, match="firstName is required for individual members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="INDIVIDUAL",
|
||||
first_name="",
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_individual_with_whitespace_first_name_invalid(self):
|
||||
"""Test INDIVIDUAL with whitespace-only firstName fails."""
|
||||
with pytest.raises(ValidationError, match="firstName is required for individual members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="INDIVIDUAL",
|
||||
first_name=" ",
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_organization_with_company_name_valid(self):
|
||||
"""Test ORGANIZATION with companyName passes."""
|
||||
validate_member_type_requirements(
|
||||
member_type="ORGANIZATION",
|
||||
first_name=None,
|
||||
company_name="Acme Corp"
|
||||
)
|
||||
|
||||
def test_organization_without_company_name_invalid(self):
|
||||
"""Test ORGANIZATION without companyName fails."""
|
||||
with pytest.raises(ValidationError, match="companyName is required for organization members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="ORGANIZATION",
|
||||
first_name=None,
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_organization_with_empty_company_name_invalid(self):
|
||||
"""Test ORGANIZATION with empty companyName fails."""
|
||||
with pytest.raises(ValidationError, match="companyName is required for organization members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="ORGANIZATION",
|
||||
first_name=None,
|
||||
company_name=""
|
||||
)
|
||||
|
||||
def test_organization_with_whitespace_company_name_invalid(self):
|
||||
"""Test ORGANIZATION with whitespace-only companyName fails."""
|
||||
with pytest.raises(ValidationError, match="companyName is required for organization members"):
|
||||
validate_member_type_requirements(
|
||||
member_type="ORGANIZATION",
|
||||
first_name=None,
|
||||
company_name=" "
|
||||
)
|
||||
|
||||
def test_invalid_member_type_raises_error(self):
|
||||
"""Test invalid member type raises ValidationError."""
|
||||
with pytest.raises(ValidationError, match="Invalid member type"):
|
||||
validate_member_type_requirements(
|
||||
member_type="INVALID",
|
||||
first_name="John",
|
||||
company_name=None
|
||||
)
|
||||
|
||||
def test_organization_can_have_first_name_optional(self):
|
||||
"""Test ORGANIZATION can optionally have firstName."""
|
||||
validate_member_type_requirements(
|
||||
member_type="ORGANIZATION",
|
||||
first_name="John Doe",
|
||||
company_name="Acme Corp"
|
||||
)
|
||||
|
||||
def test_individual_can_have_company_name_optional(self):
|
||||
"""Test INDIVIDUAL can optionally have companyName (edge case)."""
|
||||
validate_member_type_requirements(
|
||||
member_type="INDIVIDUAL",
|
||||
first_name="John",
|
||||
company_name="Acme Corp"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user