chore: add production deployment config and upgrade path requirements

- Add docker-compose.yml and docker-compose.example.yml for production deployment
- Add .env.example with all required environment variables
- Update architect agent with upgrade path requirements
- Update developer agent with migration best practices
- Add Phase 3 design documents (v0.3.0)
- Add ADR-0006 for participant state management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 19:32:42 -07:00
parent 155bd5fcf3
commit 915e77d994
11 changed files with 4272 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
# Phase 3 (v0.3.0) Design Documentation
**Version**: 0.3.0
**Date**: 2025-12-22
**Status**: Ready for Implementation
## Quick Start
Phase 3 implements participant self-management features, building on the authentication foundation from Phase 2.
**Core Features**:
- Participant list view (see who else registered)
- Profile updates (name and gift ideas)
- Reminder email preferences
- Participant withdrawal (before registration closes)
## Document Index
### 1. [System Overview](overview.md)
High-level architecture, goals, and design decisions for Phase 3.
**Read this first** to understand:
- What's in scope for v0.3.0
- Key design decisions (profile locks, withdrawal rules)
- Data flow diagrams
- State machine changes
**Key sections**:
- Phase Goals (page 1)
- Key Design Decisions (page 2)
- State Machine Impact (page 4)
### 2. [Participant Self-Management Component Design](participant-self-management.md)
Detailed component specifications for all Phase 3 features.
**Use this for**:
- Exact function signatures and implementations
- Form definitions
- Route specifications
- Template structures
- Email templates
**Key sections**:
- Business Logic Functions (page 1)
- Forms (page 6)
- Routes (page 8)
- Templates (page 11)
- Security Checklist (page 19)
### 3. [Test Plan](test-plan.md)
Comprehensive testing specifications for Phase 3.
**Use this for**:
- Unit test cases and fixtures
- Integration test scenarios
- Acceptance test procedures
- Manual QA steps
- Coverage requirements
**Key sections**:
- Unit Tests (page 1)
- Integration Tests (page 4)
- Acceptance Tests (page 9)
- Edge Cases (page 11)
### 4. [Implementation Guide](implementation-guide.md)
Step-by-step implementation instructions using TDD.
**Follow this to**:
- Implement features in correct order
- Write tests first, then code
- Verify each feature before moving on
- Create proper commits and PRs
**Key sections**:
- Implementation Order (page 1)
- Phase 3.1: Participant List (page 2)
- Phase 3.2: Profile Updates (page 4)
- Phase 3.3: Reminder Preferences (page 7)
- Phase 3.4: Withdrawal (page 9)
- Final Steps (page 14)
## Architecture Decision Records
### [ADR-0006: Participant State Management](../../decisions/0006-participant-state-management.md)
Documents key decisions about when participants can update profiles, withdraw, and how withdrawals are implemented.
**Key decisions**:
- Profile updates allowed until matching
- Withdrawals allowed until registration closes
- Soft delete implementation (withdrawn_at timestamp)
- Withdrawn participants visible only to admin
- No re-registration with same email
## User Stories Implemented
Phase 3 completes these backlog items:
### Epic 4: Participant Registration
-**Story 4.5**: View Participant List (Pre-Matching)
### Epic 6: Participant Self-Management
-**Story 6.1**: Update Profile
-**Story 6.2**: Withdraw from Exchange
-**Story 6.3**: Update Reminder Preferences
## Dependencies
### Prerequisites (from Phase 2)
- ✅ Participant model with `withdrawn_at` field
- ✅ Participant authentication (@participant_required decorator)
- ✅ Participant dashboard route
- ✅ Email service for sending emails
### No New Dependencies
Phase 3 requires **no new**:
- Python packages
- Database migrations
- Environment variables
- External services
## Technical Highlights
### New Files Created
```
src/
utils/participant.py # Business logic functions
services/withdrawal.py # Withdrawal service
templates/
participant/
profile_edit.html # Profile edit page
withdraw.html # Withdrawal confirmation
emails/
participant/
withdrawal_confirmation.html # Withdrawal email
withdrawal_confirmation.txt # Plain text version
tests/
unit/
test_participant_utils.py # Unit tests for business logic
test_withdrawal_service.py # Unit tests for withdrawal
integration/
test_profile_update.py # Profile update integration tests
test_withdrawal.py # Withdrawal integration tests
test_participant_list.py # Participant list tests
test_reminder_preferences.py # Preference update tests
```
### Modified Files
```
src/
routes/participant.py # New routes added
forms/participant.py # New forms added
services/email.py # Withdrawal email method added
templates/
participant/dashboard.html # Enhanced with participant list
```
## Design Principles Applied
Phase 3 adheres to project principles:
1. **Simplicity First**
- No new database tables (uses existing fields)
- No new external dependencies
- Soft delete instead of complex cascade handling
2. **State-Based Permissions**
- Clear rules about when operations are allowed
- Based on exchange state (draft, open, closed, matched)
- Easy to test and reason about
3. **TDD Approach**
- Implementation guide follows test-first methodology
- Every feature has unit and integration tests
- 80%+ coverage maintained
4. **Security by Design**
- All routes require authentication
- State validation prevents unauthorized operations
- CSRF protection on all POST operations
- Input sanitization via WTForms and Jinja2
5. **Privacy-Conscious**
- Participant list shows names only (no emails)
- Withdrawn participants hidden from other participants
- Gift ideas not revealed until matching
## Implementation Checklist
Use this to track progress:
- [ ] **Phase 3.1: Participant List View**
- [ ] Create `src/utils/participant.py`
- [ ] Write unit tests for utility functions
- [ ] Update dashboard route
- [ ] Update dashboard template
- [ ] Write integration tests
- [ ] Manual QA
- [ ] **Phase 3.2: Profile Updates**
- [ ] Add `can_update_profile()` function
- [ ] Write unit tests for state validation
- [ ] Create `ProfileUpdateForm`
- [ ] Create profile edit route
- [ ] Create profile edit template
- [ ] Update dashboard with edit link
- [ ] Write integration tests
- [ ] Manual QA
- [ ] **Phase 3.3: Reminder Preferences**
- [ ] Create `ReminderPreferenceForm`
- [ ] Create preference update route
- [ ] Update dashboard with preference form
- [ ] Write integration tests
- [ ] Manual QA
- [ ] **Phase 3.4: Withdrawal**
- [ ] Add `can_withdraw()` function
- [ ] Create `src/services/withdrawal.py`
- [ ] Write unit tests for withdrawal service
- [ ] Create `WithdrawForm`
- [ ] Create withdrawal route
- [ ] Create email templates
- [ ] Update email service
- [ ] Create withdrawal template
- [ ] Update dashboard with withdraw link
- [ ] Write integration tests
- [ ] Manual QA
- [ ] **Final Steps**
- [ ] Run all tests (≥ 80% coverage)
- [ ] Run linting and type checking
- [ ] Complete manual QA from test plan
- [ ] Update documentation if needed
- [ ] Create feature branch
- [ ] Commit changes
- [ ] Create pull request
## Success Criteria
Phase 3 is complete when:
1. ✅ All user stories have passing acceptance tests
2. ✅ Code coverage ≥ 80%
3. ✅ All linting and type checking passes
4. ✅ Manual QA completed
5. ✅ Security checklist verified
6. ✅ Accessibility tests pass
7. ✅ Browser compatibility verified
8. ✅ Phase 2 regression tests still pass
9. ✅ Documentation updated
10. ✅ Pull request approved and merged
## What's Next: Phase 4
After Phase 3 is complete, the next logical phase is:
**Phase 4 (v0.4.0)**: Post-Matching Participant Experience (Epic 11)
- View assigned recipient (match assignment)
- View recipient's gift ideas
- View exchange information post-matching
- Participant list (post-matching version)
This builds on Phase 3's participant dashboard foundation and enables the core Secret Santa experience after admin has matched participants.
## Questions?
- Review the [Project Overview](../../PROJECT_OVERVIEW.md) for product vision
- Check the [Backlog](../../BACKLOG.md) for user stories
- See [v0.2.0 Design](../v0.2.0/overview.md) for foundation architecture
- Consult existing [ADRs](../../decisions/) for architectural context
---
**Phase 3 Design Status**: ✅ Complete and Ready for Implementation

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,566 @@
# System Overview - v0.3.0
**Version**: 0.3.0
**Date**: 2025-12-22
**Status**: Phase 3 Design
## Introduction
This document describes the design for Phase 3 of Sneaky Klaus, building on the participant authentication foundation established in v0.2.0. This phase focuses on participant self-management capabilities and pre-matching participant experience.
**Phase 3 Scope**: Implement Epic 6 (Participant Self-Management) and Epic 4.5 (View Participant List Pre-Matching), enabling participants to manage their own profiles and see who else has registered before matching occurs.
## Phase Goals
The primary goals for v0.3.0 are:
1. **Enable participant self-service**: Allow participants to update their profiles without admin intervention
2. **Support graceful withdrawals**: Handle participants who need to drop out before matching
3. **Build social engagement**: Let participants see who else is participating (pre-matching only)
4. **Maintain data integrity**: Ensure profile changes and withdrawals don't break the exchange flow
## User Stories in Scope
### Epic 6: Participant Self-Management
- **6.1 Update Profile**: Participants can edit their name and gift ideas before matching
- **6.2 Withdraw from Exchange**: Participants can opt out before registration closes
- **6.3 Update Reminder Preferences**: Participants can toggle reminder emails on/off
### Epic 4: Participant Registration (Continuation)
- **4.5 View Participant List (Pre-Matching)**: Registered participants can see who else has joined
## Out of Scope for v0.3.0
The following features are explicitly **not** included in this phase:
- Post-matching participant experience (Epic 11) - deferred to Phase 4
- Matching system (Epic 8) - deferred to Phase 5
- Admin exchange management beyond what v0.1.0 provided
- Participant removal by admin (Epic 9) - deferred to Phase 6
- Notification emails beyond registration confirmation (Epic 10) - partial in Phase 2, remainder in Phase 7
## Architecture Overview
Phase 3 builds incrementally on the v0.2.0 architecture with no new infrastructure components:
```mermaid
graph TB
subgraph "Phase 3 Additions"
ProfileUpdate[Profile Update UI]
WithdrawUI[Withdraw UI]
ParticipantList[Participant List View]
ProfileUpdate --> ParticipantRoutes
WithdrawUI --> ParticipantRoutes
ParticipantList --> ParticipantRoutes
end
subgraph "Existing v0.2.0 Foundation"
ParticipantRoutes[Participant Routes]
ParticipantAuth[Participant Auth Decorator]
ParticipantModel[Participant Model]
ExchangeModel[Exchange Model]
Database[(SQLite)]
ParticipantRoutes --> ParticipantAuth
ParticipantRoutes --> ParticipantModel
ParticipantRoutes --> ExchangeModel
ParticipantModel --> Database
ExchangeModel --> Database
end
```
## Technical Design Principles
This phase adheres to the project's core principles established in v0.1.0 and v0.2.0:
1. **No new database tables**: All features use existing Participant and Exchange models
2. **No new external dependencies**: Pure Flask/SQLAlchemy implementation
3. **Soft deletes**: Withdrawals use `withdrawn_at` timestamp rather than hard deletes
4. **State-based permissions**: Operations restricted based on exchange state
5. **TDD approach**: Write tests first, implement to pass
## Key Design Decisions
### 1. Profile Update Restrictions
**Decision**: Profile updates are only allowed when exchange is in `draft`, `registration_open`, or `registration_closed` states. After matching, profiles are locked.
**Rationale**:
- Gift ideas are sent to givers during match notification - changes after matching would create inconsistency
- Name changes after matching could confuse participants
- Clear state transition prevents data inconsistency
**Implementation**:
```python
def can_update_profile(participant: Participant) -> bool:
"""Check if participant can update their profile."""
exchange = participant.exchange
return exchange.state in ['draft', 'registration_open', 'registration_closed']
```
### 2. Withdrawal Rules
**Decision**: Withdrawals are only allowed before registration closes. After registration closes, admin intervention required.
**Rationale**:
- Pre-closure: minimal impact, just removes one participant
- Post-closure: admin likely already configuring exclusions or has matched
- Post-matching: requires re-match, admin should make this decision
- Clear deadline prevents last-minute dropouts
**Implementation**:
```python
def can_withdraw(participant: Participant) -> bool:
"""Check if participant can withdraw themselves."""
exchange = participant.exchange
return exchange.state in ['draft', 'registration_open']
```
### 3. Withdrawal Implementation (Soft Delete)
**Decision**: Use existing `withdrawn_at` timestamp field, don't delete participant records.
**Rationale**:
- Data model already supports soft deletes (v0.2.0 design)
- Preserves audit trail of who registered
- Prevents re-use of email during same exchange
- Allows admin to see withdrawal history
- Simplifies database constraints (no cascade delete issues)
**Implementation**:
```python
def withdraw_participant(participant: Participant):
"""Mark participant as withdrawn."""
participant.withdrawn_at = datetime.utcnow()
db.session.commit()
```
### 4. Participant List Visibility
**Decision**: Show participant list to all registered participants, but only show names (not emails or gift ideas).
**Rationale**:
- Social aspect: participants want to know who's participating
- Privacy: emails are admin-only
- Security: gift ideas are for givers only (post-matching)
- Pre-matching only: post-matching handled in Phase 4
**Display Rules**:
- Show: participant names, count
- Hide: emails, gift ideas, withdrawn participants
- Filter: exclude withdrawn participants from list
### 5. Reminder Preference Changes
**Decision**: Allow reminder preference changes at any time before exchange completes.
**Rationale**:
- Low-impact change (doesn't affect matching or other participants)
- User preference should be flexible
- No technical reason to restrict after matching
- Allows opting out if circumstances change
**Implementation**: Simple boolean toggle, no state restrictions.
### 6. Form Validation Strategy
**Decision**: Use server-side validation with WTForms, add client-side hints for UX.
**Rationale**:
- Consistent with Phase 2 implementation decisions
- Security: never trust client-side validation
- UX: client-side provides immediate feedback
- Progressive enhancement: works without JavaScript
## Data Flow
### Profile Update Flow
```mermaid
sequenceDiagram
participant P as Participant Browser
participant F as Flask App
participant DB as SQLite Database
P->>F: GET /participant/profile/edit
F->>F: Check @participant_required
F->>DB: Load participant & exchange
F->>F: Check can_update_profile()
F-->>P: Render edit form (or error)
P->>F: POST /participant/profile/edit (name, gift_ideas)
F->>F: Validate form
F->>F: Check can_update_profile()
F->>DB: Update participant record
DB-->>F: Success
F-->>P: Redirect to dashboard with success message
```
### Withdrawal Flow
```mermaid
sequenceDiagram
participant P as Participant Browser
participant F as Flask App
participant DB as SQLite Database
participant E as Email Service
P->>F: POST /participant/withdraw (with confirmation)
F->>F: Check @participant_required
F->>DB: Load participant & exchange
F->>F: Check can_withdraw()
F->>DB: Set withdrawn_at timestamp
DB-->>F: Success
F->>E: Send withdrawal confirmation email
E-->>F: Email sent
F->>F: Clear session (log out participant)
F-->>P: Redirect to public page with confirmation
```
### Participant List Flow
```mermaid
sequenceDiagram
participant P as Participant Browser
participant F as Flask App
participant DB as SQLite Database
P->>F: GET /participant/dashboard
F->>F: Check @participant_required
F->>DB: Load participant's exchange
F->>DB: Query active participants (withdrawn_at IS NULL)
DB-->>F: Participant list (names only)
F-->>P: Render dashboard with participant list
```
## State Machine Impact
Phase 3 doesn't add new exchange states, but adds participant-level state:
```mermaid
stateDiagram-v2
[*] --> Registered: Registration
Registered --> UpdatedProfile: Edit profile
UpdatedProfile --> UpdatedProfile: Edit again
UpdatedProfile --> Withdrawn: Withdraw
Registered --> Withdrawn: Withdraw
UpdatedProfile --> Matched: Admin matches
Registered --> Matched: Admin matches
Withdrawn --> [*]
note right of Withdrawn
Soft delete: withdrawn_at set
Cannot re-activate
Must re-register with new email
end note
note right of UpdatedProfile
Only before matching
Name and gift_ideas editable
end note
```
## Component Design
### Routes (participant_bp)
New routes added to existing `src/routes/participant.py`:
| Route | Method | Auth | Description |
|-------|--------|------|-------------|
| `/participant/profile/edit` | GET, POST | @participant_required | Edit profile (name, gift ideas) |
| `/participant/preferences` | POST | @participant_required | Update reminder preference |
| `/participant/withdraw` | POST | @participant_required | Withdraw from exchange |
Existing routes used:
- `/participant/dashboard` - enhanced to show participant list
### Forms (src/forms/participant.py)
New forms:
**ProfileUpdateForm**:
```python
class ProfileUpdateForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(max=255)])
gift_ideas = TextAreaField('Gift Ideas', validators=[Length(max=10000)])
submit = SubmitField('Save Changes')
```
**ReminderPreferenceForm**:
```python
class ReminderPreferenceForm(FlaskForm):
reminder_enabled = BooleanField('Send me reminders')
submit = SubmitField('Update Preferences')
```
**WithdrawForm**:
```python
class WithdrawForm(FlaskForm):
confirm = BooleanField('I understand this cannot be undone',
validators=[DataRequired()])
submit = SubmitField('Withdraw from Exchange')
```
### Templates
New templates in `templates/participant/`:
- `profile_edit.html` - Profile update form
- `withdraw.html` - Withdrawal confirmation page (with warnings)
- `participant_list.html` - Reusable component for displaying participant names
Enhanced templates:
- `dashboard.html` - Add participant list section
### Email Templates
New email template in `templates/emails/participant/`:
- `withdrawal_confirmation.html` - Sent when participant withdraws
- `withdrawal_confirmation.txt` - Plain text version
## Security Considerations
### 1. Authorization Checks
Every participant operation must verify:
- User is authenticated as participant (@participant_required)
- User owns the resource (participant_id matches session)
- Exchange state allows the operation (can_update_profile, can_withdraw)
### 2. CSRF Protection
All POST operations require CSRF tokens (WTForms automatic).
### 3. Input Validation
- Name: 1-255 characters, required
- Gift ideas: 0-10,000 characters, optional
- All inputs sanitized via Jinja2 auto-escaping
### 4. Privacy
Participant list shows:
- Names only (public within exchange)
- Not emails (admin-only)
- Not gift ideas (giver-only post-matching)
- Not withdrawn participants (respect withdrawal privacy)
### 5. Rate Limiting
No new rate limiting needed:
- Profile updates: legitimate user operation, no abuse vector
- Withdrawals: self-limiting (can only withdraw once)
- Participant list: read-only, authenticated
## Testing Strategy
### Unit Tests
Test business logic in isolation:
- `can_update_profile()` logic for each exchange state
- `can_withdraw()` logic for each exchange state
- Soft delete implementation (withdrawn_at timestamp)
- Participant list filtering (exclude withdrawn)
### Integration Tests
Test full request/response cycles:
- Profile update happy path
- Profile update when locked (post-matching)
- Withdrawal happy path
- Withdrawal when not allowed
- Participant list rendering
### Test Data Setup
Reuse existing test fixtures from Phase 2, add:
- Multiple participants in same exchange
- Participants in different states (registered, withdrawn)
- Exchanges in different states
### Coverage Target
Maintain 80%+ coverage established in previous phases.
## Error Handling
### Expected Errors
| Scenario | HTTP Status | User Message | Action |
|----------|-------------|--------------|--------|
| Update profile after matching | 400 | "Your profile is locked after matching" | Redirect to dashboard |
| Withdraw after registration closes | 400 | "Withdrawal deadline has passed. Contact admin." | Redirect to dashboard |
| Invalid form data | 400 | Field-specific errors | Re-render form with errors |
| Already withdrawn | 400 | "You have already withdrawn" | Redirect to public page |
### Unexpected Errors
- Database errors: Log, flash generic error, redirect safely
- Missing participant record: Clear session, redirect to landing page
- Orphaned session: Clear session, redirect to landing page
## Performance Considerations
### Database Queries
**Participant List Query**:
```python
# Efficient query with single DB hit
participants = Participant.query.filter(
Participant.exchange_id == exchange_id,
Participant.withdrawn_at.is_(None)
).order_by(Participant.name).all()
```
**Profile Load**:
- Already loaded by @participant_required decorator (uses `g.participant`)
- No additional query needed
### Caching
Not needed for v0.3.0:
- Participant counts are small (3-100 typical)
- Updates are infrequent
- Reads are authenticated (no public caching)
## Migration Requirements
**No database migrations required for v0.3.0.**
All fields already exist:
- `participant.name` - editable
- `participant.gift_ideas` - editable
- `participant.reminder_enabled` - editable
- `participant.withdrawn_at` - set on withdrawal
## Deployment Impact
### Breaking Changes
None. v0.3.0 is fully backward compatible with v0.2.0.
### Configuration Changes
None. No new environment variables or configuration needed.
### Data Migration
None. Existing data fully compatible.
## User Experience Flow
### Participant Journey (Pre-Matching)
1. **Register** (Phase 2)
- Receive confirmation email with magic link
2. **View Dashboard** (Phase 2 + Phase 3)
- See own information
- **NEW**: See list of other participants (names only)
3. **Update Profile** (Phase 3)
- Click "Edit Profile" from dashboard
- Update name or gift ideas
- Save changes
- See confirmation message
4. **Change Reminder Preference** (Phase 3)
- Toggle reminder checkbox on dashboard
- See confirmation message
5. **Withdraw (if needed)** (Phase 3)
- Click "Withdraw" from dashboard
- See warning about permanence
- Confirm withdrawal
- Receive confirmation email
- Logged out and redirected to public page
## Admin Experience Impact
Phase 3 adds visibility for admins:
### Exchange Detail View (Enhancement)
Add to existing exchange detail page:
- Show participant list with withdrawn indicator
- Display: "5 active, 1 withdrawn"
- Allow admin to see withdrawn participants (grayed out)
**Implementation Note**: This is a minor enhancement to existing admin routes, not a major feature. Can be implemented alongside participant features or deferred to Phase 6 (Admin Participant Management).
## Future Considerations
### Phase 4 Dependencies
Phase 3 sets up foundations for Phase 4 (Post-Matching Experience):
- Participant list already implemented (will be reused post-matching)
- Profile lock logic (can_update_profile) prevents changes after matching
- Dashboard structure ready to show match assignment
### Potential Enhancements (Out of Scope)
Not included in v0.3.0 but possible future additions:
- Email notification to admin when participant withdraws (Epic 10.5)
- Allow participants to indicate dietary restrictions or allergies
- Allow participants to add profile pictures
- Allow participants to message admin (anonymous contact form)
## Acceptance Criteria
Phase 3 is complete when:
1. ✅ Participants can update their name and gift ideas before matching
2. ✅ Participants cannot update profile after matching occurs
3. ✅ Participants can withdraw before registration closes
4. ✅ Participants cannot withdraw after registration closes
5. ✅ Withdrawn participants receive confirmation email
6. ✅ Withdrawn participants are logged out and removed from participant list
7. ✅ Participants can view list of other registered participants (names only)
8. ✅ Participant list excludes withdrawn participants
9. ✅ Participants can toggle reminder preferences at any time
10. ✅ All operations require participant authentication
11. ✅ All operations validate exchange state appropriately
12. ✅ All user stories have passing integration tests
13. ✅ Code coverage remains at 80%+
## Implementation Phases
Recommended implementation order (TDD, vertical slices):
### Phase 3.1: Participant List View
- Story 4.5: View Participant List (Pre-Matching)
- Simplest feature, no state changes
- Sets up dashboard enhancements
### Phase 3.2: Profile Updates
- Story 6.1: Update Profile
- Core self-management feature
- Tests state-based permissions
### Phase 3.3: Reminder Preferences
- Story 6.3: Update Reminder Preferences
- Simple toggle, low risk
- Quick win
### Phase 3.4: Withdrawal
- Story 6.2: Withdraw from Exchange
- Most complex (state changes, email, logout)
- Benefits from previous features being solid
## References
- [v0.2.0 System Overview](../v0.2.0/overview.md)
- [v0.2.0 Data Model](../v0.2.0/data-model.md)
- [Product Backlog](../../BACKLOG.md)
- [Project Overview](../../PROJECT_OVERVIEW.md)
- [ADR-0002: Authentication Strategy](../../decisions/0002-authentication-strategy.md)
- [ADR-0003: Participant Session Scoping](../../decisions/0003-participant-session-scoping.md)
---
**End of Phase 3 Design Overview**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,545 @@
# Test Plan - v0.3.0
**Version**: 0.3.0
**Date**: 2025-12-22
**Status**: Test Specification
## Overview
This document defines the comprehensive test plan for Phase 3 (Participant Self-Management). It covers unit tests, integration tests, and acceptance criteria for all user stories in scope.
## Test Pyramid
```
╱╲
╲ E2E Tests (Manual QA)
╱────╲ - Full user journeys
╲ - Cross-browser testing
╱────────╲
╲ Integration Tests (pytest)
╱────────────╲ - Route handlers
╲ - Database operations
╱────────────────╲ - Email sending
╱──────────────────╲
Unit Tests (pytest)
- Business logic functions
- Form validation
- State checks
```
## Test Coverage Goals
- **Overall coverage**: 80%+ (maintain Phase 2 level)
- **Business logic**: 95%+ (pure functions)
- **Route handlers**: 80%+ (integration tests)
- **Templates**: Manual testing (not measured by coverage)
## 1. Unit Tests
### 1.1 Participant Utility Functions
**File**: `tests/unit/test_participant_utils.py`
| Test Case | Description | Assertions |
|-----------|-------------|------------|
| `test_can_update_profile_draft_state` | Profile updates allowed in draft | `can_update_profile() == True` |
| `test_can_update_profile_registration_open` | Profile updates allowed when open | `can_update_profile() == True` |
| `test_can_update_profile_registration_closed` | Profile updates allowed when closed | `can_update_profile() == True` |
| `test_can_update_profile_matched_state` | Profile updates blocked after matching | `can_update_profile() == False` |
| `test_can_update_profile_completed_state` | Profile updates blocked when completed | `can_update_profile() == False` |
| `test_can_withdraw_draft_state` | Withdrawal allowed in draft | `can_withdraw() == True` |
| `test_can_withdraw_registration_open` | Withdrawal allowed when open | `can_withdraw() == True` |
| `test_can_withdraw_registration_closed` | Withdrawal blocked when closed | `can_withdraw() == False` |
| `test_can_withdraw_matched_state` | Withdrawal blocked after matching | `can_withdraw() == False` |
| `test_can_withdraw_already_withdrawn` | Withdrawal blocked if already withdrawn | `can_withdraw() == False` |
| `test_get_active_participants` | Returns only non-withdrawn participants | Count and names match |
| `test_get_active_participants_empty` | Returns empty list when all withdrawn | `len(participants) == 0` |
| `test_get_active_participants_ordered` | Participants ordered by name | Names in alphabetical order |
| `test_is_withdrawn_true` | Detects withdrawn participant | `is_withdrawn() == True` |
| `test_is_withdrawn_false` | Detects active participant | `is_withdrawn() == False` |
**Fixtures needed**:
```python
@pytest.fixture
def exchange_factory(db):
"""Factory for creating exchanges in different states."""
def _create(state='draft'):
exchange = Exchange(
slug=generate_slug(),
name='Test Exchange',
budget='$25-50',
max_participants=50,
registration_close_date=datetime.utcnow() + timedelta(days=7),
exchange_date=datetime.utcnow() + timedelta(days=14),
timezone='UTC',
state=state
)
db.session.add(exchange)
db.session.commit()
return exchange
return _create
@pytest.fixture
def participant_factory(db):
"""Factory for creating participants."""
def _create(exchange=None, withdrawn_at=None):
if not exchange:
exchange = Exchange(...) # Create default exchange
db.session.add(exchange)
participant = Participant(
exchange_id=exchange.id,
name='Test Participant',
email='test@example.com',
gift_ideas='Test ideas',
reminder_enabled=True,
withdrawn_at=withdrawn_at
)
db.session.add(participant)
db.session.commit()
return participant
return _create
```
### 1.2 Withdrawal Service
**File**: `tests/unit/test_withdrawal_service.py`
| Test Case | Description | Assertions |
|-----------|-------------|------------|
| `test_withdraw_participant_success` | Happy path withdrawal | `withdrawn_at` is set, email called |
| `test_withdraw_participant_already_withdrawn` | Raises error if already withdrawn | `WithdrawalError` raised |
| `test_withdraw_participant_wrong_state_closed` | Raises error if registration closed | `WithdrawalError` with specific message |
| `test_withdraw_participant_wrong_state_matched` | Raises error if already matched | `WithdrawalError` with specific message |
| `test_withdraw_participant_database_error` | Handles DB error gracefully | `WithdrawalError` raised, rollback called |
| `test_withdraw_participant_email_failure` | Continues if email fails | `withdrawn_at` set, error logged |
**Mocking strategy**:
```python
@pytest.fixture
def mock_email_service(monkeypatch):
"""Mock EmailService for withdrawal tests."""
mock = Mock()
monkeypatch.setattr('src.services.withdrawal.EmailService', lambda: mock)
return mock
```
### 1.3 Form Validation
**File**: `tests/unit/test_participant_forms.py`
| Test Case | Description | Assertions |
|-----------|-------------|------------|
| `test_profile_form_valid_data` | Valid name and gift ideas | `form.validate() == True` |
| `test_profile_form_name_required` | Name is required | Validation error on name field |
| `test_profile_form_name_too_long` | Name max 255 chars | Validation error on name field |
| `test_profile_form_gift_ideas_optional` | Gift ideas can be empty | `form.validate() == True` |
| `test_profile_form_gift_ideas_too_long` | Gift ideas max 10,000 chars | Validation error on gift_ideas field |
| `test_reminder_form_boolean_field` | Accepts boolean value | `form.validate() == True` |
| `test_withdraw_form_confirmation_required` | Confirmation required | Validation error on confirm field |
| `test_withdraw_form_confirmation_true` | Accepts confirmation | `form.validate() == True` |
## 2. Integration Tests
### 2.1 Profile Update Tests
**File**: `tests/integration/test_profile_update.py`
| Test Case | Description | Setup | Action | Expected Result |
|-----------|-------------|-------|--------|-----------------|
| `test_profile_update_get_shows_form` | GET shows edit form | Auth'd participant | GET /participant/profile/edit | 200, form with current values |
| `test_profile_update_post_success` | POST updates profile | Auth'd participant | POST with valid data | 302 redirect, flash success, DB updated |
| `test_profile_update_name_change` | Name updates in DB | Auth'd participant | POST with new name | Participant.name updated |
| `test_profile_update_gift_ideas_change` | Gift ideas update in DB | Auth'd participant | POST with new ideas | Participant.gift_ideas updated |
| `test_profile_update_clears_whitespace` | Strips leading/trailing spaces | Auth'd participant | POST with " Name " | Stored as "Name" |
| `test_profile_update_locked_after_matching` | Blocked when matched | Matched exchange | GET profile edit | 302 redirect, flash error |
| `test_profile_update_form_validation_error` | Invalid data shows errors | Auth'd participant | POST with empty name | 200, form with errors |
| `test_profile_update_csrf_required` | CSRF token required | Auth'd participant | POST without CSRF | 400 error |
| `test_profile_update_requires_auth` | Auth required | No session | GET profile edit | 302 to login |
| `test_profile_update_database_error` | Handles DB failure | Auth'd participant, mock DB error | POST valid data | Flash error, no DB change |
**Test helpers**:
```python
@pytest.fixture
def participant_session(client, participant_factory):
"""Create authenticated participant session."""
participant = participant_factory()
with client.session_transaction() as session:
session['user_id'] = participant.id
session['user_type'] = 'participant'
session['exchange_id'] = participant.exchange_id
return participant
def get_csrf_token(client, url='/participant/dashboard'):
"""Extract CSRF token from page."""
response = client.get(url)
# Parse HTML and extract token
return extract_csrf_token(response.data)
```
### 2.2 Withdrawal Tests
**File**: `tests/integration/test_withdrawal.py`
| Test Case | Description | Setup | Action | Expected Result |
|-----------|-------------|-------|--------|-----------------|
| `test_withdrawal_get_shows_form` | GET shows confirmation page | Auth'd participant | GET /participant/withdraw | 200, form with warnings |
| `test_withdrawal_post_success` | POST withdraws participant | Auth'd participant | POST with confirmation | 302 redirect, withdrawn_at set, session cleared |
| `test_withdrawal_sends_email` | Email sent on withdrawal | Auth'd participant, mock email | POST with confirmation | Email service called |
| `test_withdrawal_clears_session` | Session cleared after withdrawal | Auth'd participant | POST with confirmation | Session empty |
| `test_withdrawal_redirects_to_public` | Redirects to registration page | Auth'd participant | POST with confirmation | Redirect to /exchange/{slug}/register |
| `test_withdrawal_already_withdrawn` | Detects already withdrawn | Withdrawn participant | GET withdraw | Flash info, redirect |
| `test_withdrawal_blocked_after_close` | Blocked when registration closed | Closed exchange | GET withdraw | Flash error, redirect to dashboard |
| `test_withdrawal_blocked_after_matching` | Blocked when matched | Matched exchange | GET withdraw | Flash error, redirect to dashboard |
| `test_withdrawal_requires_confirmation` | Confirmation checkbox required | Auth'd participant | POST without confirm=True | Form errors |
| `test_withdrawal_csrf_required` | CSRF token required | Auth'd participant | POST without CSRF | 400 error |
| `test_withdrawal_requires_auth` | Auth required | No session | GET withdraw | 302 to login |
| `test_withdrawal_database_error` | Handles DB failure gracefully | Auth'd participant, mock DB error | POST valid data | Flash error, not withdrawn |
### 2.3 Reminder Preference Tests
**File**: `tests/integration/test_reminder_preferences.py`
| Test Case | Description | Setup | Action | Expected Result |
|-----------|-------------|-------|--------|-----------------|
| `test_update_preferences_enable` | Enable reminders | Auth'd participant (disabled) | POST reminder_enabled=True | Flash success, DB updated |
| `test_update_preferences_disable` | Disable reminders | Auth'd participant (enabled) | POST reminder_enabled=False | Flash success, DB updated |
| `test_update_preferences_csrf_required` | CSRF token required | Auth'd participant | POST without CSRF | 400 error |
| `test_update_preferences_requires_auth` | Auth required | No session | POST preferences | 302 to login |
| `test_update_preferences_database_error` | Handles DB failure | Auth'd participant, mock DB error | POST valid data | Flash error, no change |
### 2.4 Participant List Tests
**File**: `tests/integration/test_participant_list.py`
| Test Case | Description | Setup | Action | Expected Result |
|-----------|-------------|-------|--------|-----------------|
| `test_participant_list_shows_all_active` | Shows all active participants | 3 active participants | GET dashboard | All 3 names displayed |
| `test_participant_list_excludes_withdrawn` | Hides withdrawn participants | 2 active, 1 withdrawn | GET dashboard | Only 2 names displayed |
| `test_participant_list_shows_self_badge` | Marks current user | Auth'd participant | GET dashboard | "You" badge on own name |
| `test_participant_list_ordered_by_name` | Alphabetical order | Participants: Zoe, Alice, Bob | GET dashboard | Order: Alice, Bob, Zoe |
| `test_participant_list_count_excludes_withdrawn` | Count shows active only | 3 active, 1 withdrawn | GET dashboard | "Participants (3)" |
| `test_participant_list_empty_state` | Message when alone | Only current participant | GET dashboard | "No other participants yet" |
| `test_participant_list_requires_auth` | Auth required | No session | GET dashboard | 302 to login |
## 3. Acceptance Tests
### 3.1 Story 4.5: View Participant List (Pre-Matching)
**Acceptance Criteria**:
- ✅ Participant list visible after logging in via magic link
- ✅ Shows display names only
- ✅ Does not show email addresses
- ✅ Does not indicate any match information
- ✅ Updates as new participants register
**Manual Test Steps**:
1. **Setup**: Create exchange, register 3 participants (Alice, Bob, Charlie)
2. **Login as Alice**: Request magic link, login
3. **View Dashboard**: Participant list shows "Bob" and "Charlie" (not Alice's own name in list)
4. **Verify No Emails**: Inspect HTML, confirm no email addresses visible
5. **Register New Participant (Dave)**: Have Dave register
6. **Refresh Dashboard**: Dave now appears in Alice's participant list
7. **Bob Withdraws**: Have Bob withdraw
8. **Refresh Dashboard**: Bob no longer appears in participant list
**Expected Results**: Pass all criteria
### 3.2 Story 6.1: Update Profile
**Acceptance Criteria**:
- ✅ Edit option available when logged in
- ✅ Can update name and gift ideas
- ✅ Cannot change email (request admin help)
- ✅ Only available before matching occurs
- ✅ Confirmation after save
**Manual Test Steps**:
1. **Login as Participant**: Use magic link to access dashboard
2. **Click "Edit Profile"**: Navigate to profile edit page
3. **Verify Pre-Population**: Name and gift ideas fields show current values
4. **Update Name**: Change name from "Alice" to "Alice Smith"
5. **Update Gift Ideas**: Add new gift idea
6. **Verify Email Not Editable**: Email field not present in form
7. **Save Changes**: Submit form
8. **Verify Success Message**: "Your profile has been updated successfully"
9. **Verify Dashboard Updated**: Dashboard shows new name and gift ideas
10. **Admin Matches Exchange**: Admin triggers matching
11. **Try to Edit Profile**: Click "Edit Profile" (if visible)
12. **Verify Locked**: Error message "Your profile is locked after matching"
**Expected Results**: Pass all criteria
### 3.3 Story 6.2: Withdraw from Exchange
**Acceptance Criteria**:
- ✅ "Withdraw" option available before registration closes
- ✅ Confirmation required
- ✅ Participant removed from exchange
- ✅ Confirmation email sent
- ✅ Admin notified (if notifications enabled) - **Deferred to Phase 7**
**Manual Test Steps**:
1. **Login as Participant**: Use magic link
2. **Click "Withdraw from Exchange"**: Navigate to withdrawal page
3. **Verify Warning**: Warning box shows consequences
4. **Try Submit Without Confirmation**: Leave checkbox unchecked, submit
5. **Verify Error**: Form error requires confirmation
6. **Check Confirmation Box**: Check "I understand..." box
7. **Submit Withdrawal**: Click "Withdraw from Exchange"
8. **Verify Success Message**: "You have been withdrawn..."
9. **Verify Logged Out**: Session cleared, redirected to public page
10. **Check Email**: Withdrawal confirmation email received
11. **Login as Different Participant**: Login as Bob
12. **Check Participant List**: Withdrawn participant (Alice) not in list
13. **Admin Closes Registration**: Admin closes registration
14. **Try to Withdraw as Bob**: Navigate to withdrawal page
15. **Verify Blocked**: Error message "Registration has closed"
**Expected Results**: Pass all criteria (except admin notification - Phase 7)
### 3.4 Story 6.3: Update Reminder Preferences
**Acceptance Criteria**:
- ✅ Option to enable/disable reminder emails
- ✅ Available before exchange completes
- ✅ Changes take effect immediately
**Manual Test Steps**:
1. **Login as Participant**: Use magic link
2. **View Dashboard**: Reminder preference checkbox visible
3. **Verify Current State**: Checkbox checked (enabled by default)
4. **Uncheck Reminder Box**: Uncheck "Send me reminders"
5. **Click "Update Preferences"**: Submit form
6. **Verify Success Message**: "Reminder emails disabled"
7. **Refresh Page**: Checkbox remains unchecked
8. **Re-Enable Reminders**: Check box, submit
9. **Verify Success Message**: "Reminder emails enabled"
10. **Admin Matches Exchange**: Trigger matching
11. **Verify Still Available**: Reminder preference form still available post-matching
**Expected Results**: Pass all criteria
## 4. Edge Cases and Error Scenarios
### 4.1 Race Conditions
| Scenario | Expected Behavior | Test Method |
|----------|-------------------|-------------|
| Participant withdraws while admin is matching | Withdrawal succeeds if submitted before matching, blocked if submitted after | Manual timing test |
| Two participants update profiles simultaneously | Both succeed (no conflicts, different records) | Concurrent requests test |
| Participant updates profile while admin closes registration | Both succeed (profile lock is at matching, not close) | Manual timing test |
### 4.2 Data Validation
| Scenario | Expected Behavior | Test Method |
|----------|-------------------|-------------|
| Name with only whitespace | Validation error "Name is required" | Integration test |
| Gift ideas exactly 10,000 characters | Accepted | Integration test |
| Gift ideas 10,001 characters | Validation error | Integration test |
| Name with special characters (é, ñ, 中) | Accepted, displayed correctly | Manual test |
| XSS attempt in gift ideas | Escaped in display | Manual test |
### 4.3 Session Handling
| Scenario | Expected Behavior | Test Method |
|----------|-------------------|-------------|
| Session expires during profile edit | Redirect to login on submit | Manual test (wait for expiry) |
| Withdraw from different browser tab | Both tabs see withdrawal (one succeeds, one sees "already withdrawn") | Manual test |
| Participant deleted by admin while logged in | Next request clears session, redirects to login | Integration test |
## 5. Performance Tests
### 5.1 Query Optimization
| Test | Query Count | Execution Time |
|------|-------------|----------------|
| Load dashboard with 50 participants | 3 queries max (exchange, participant, participant list) | < 100ms |
| Update profile | 2 queries (load participant, update) | < 50ms |
| Withdraw participant | 3 queries (load, update, email) | < 100ms |
**Testing Method**: Use Flask-DebugToolbar or SQLAlchemy query logging
### 5.2 Concurrent Operations
| Test | Concurrent Requests | Expected Result |
|------|---------------------|-----------------|
| 10 participants updating profiles | 10 simultaneous | All succeed, no deadlocks |
| 5 participants viewing participant list | 5 simultaneous | All succeed, consistent data |
**Testing Method**: Use locust or manual concurrent curl requests
## 6. Browser Compatibility
**Supported Browsers** (per project requirements):
- Chrome (last 2 versions)
- Firefox (last 2 versions)
- Safari (last 2 versions)
- Edge (last 2 versions)
**Manual Tests**:
- Profile edit form renders correctly
- Character counter works (progressive enhancement)
- Withdrawal confirmation checkbox works
- CSRF tokens submitted correctly
- Flash messages display correctly
## 7. Accessibility Tests
**WCAG 2.1 AA Compliance**:
| Test | Tool | Expected Result |
|------|------|-----------------|
| Form labels | axe DevTools | All inputs have associated labels |
| Keyboard navigation | Manual | All actions accessible via keyboard |
| Screen reader | NVDA/JAWS | Forms and messages announced correctly |
| Color contrast | axe DevTools | All text meets 4.5:1 contrast ratio |
| Error messages | Manual | Errors linked to fields via ARIA |
## 8. Security Tests
### 8.1 Authentication
| Test | Method | Expected Result |
|------|--------|-----------------|
| Access profile edit without login | GET /participant/profile/edit | 302 redirect to login |
| Access withdrawal without login | GET /participant/withdraw | 302 redirect to login |
| Use expired session | Set session to past timestamp | Session cleared, redirect to login |
### 8.2 Authorization
| Test | Method | Expected Result |
|------|--------|-----------------|
| Edit another participant's profile | Manipulate user_id in session | Decorator uses session, not URL param - not vulnerable |
| Withdraw another participant | Manipulate user_id in session | Decorator uses session, not URL param - not vulnerable |
### 8.3 Input Validation
| Test | Method | Expected Result |
|------|--------|-----------------|
| SQL injection in name field | Submit `'; DROP TABLE participants; --` | Escaped, no SQL execution |
| XSS in gift ideas | Submit `<script>alert('XSS')</script>` | Escaped, rendered as text |
| CSRF attack | POST without token | 400 error |
## 9. Test Automation
### 9.1 CI Pipeline
```yaml
# .github/workflows/test.yml (example)
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install uv
run: pip install uv
- name: Install dependencies
run: uv sync
- name: Run unit tests
run: uv run pytest tests/unit -v --cov
- name: Run integration tests
run: uv run pytest tests/integration -v --cov --cov-append
- name: Coverage report
run: uv run coverage report --fail-under=80
```
### 9.2 Pre-Commit Hooks
```yaml
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: pytest
name: pytest
entry: uv run pytest tests/unit
language: system
pass_filenames: false
always_run: true
```
## 10. Test Data Management
### 10.1 Fixtures
**Location**: `tests/conftest.py`
```python
@pytest.fixture
def app():
"""Create test Flask app."""
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture
def exchange(db):
"""Create test exchange."""
exchange = Exchange(...)
db.session.add(exchange)
db.session.commit()
return exchange
@pytest.fixture
def participant(db, exchange):
"""Create test participant."""
participant = Participant(exchange=exchange, ...)
db.session.add(participant)
db.session.commit()
return participant
```
### 10.2 Test Database
- Use in-memory SQLite for speed: `sqlite:///:memory:`
- Reset between tests: `db.drop_all()` + `db.create_all()`
- Factories for generating test data with variations
## 11. Regression Tests
Tests from Phase 2 that must still pass:
- ✅ Participant registration still works
- ✅ Magic link authentication still works
- ✅ Participant dashboard loads (now with participant list)
- ✅ Admin login still works
- ✅ Exchange creation still works
## 12. Success Criteria
Phase 3 tests are complete when:
1. ✅ All unit tests pass (100% pass rate)
2. ✅ All integration tests pass (100% pass rate)
3. ✅ Code coverage ≥ 80%
4. ✅ All acceptance criteria manually verified
5. ✅ No security vulnerabilities found
6. ✅ Accessibility tests pass
7. ✅ All edge cases handled gracefully
8. ✅ Performance benchmarks met
9. ✅ Browser compatibility verified
10. ✅ Phase 2 regression tests pass
---
**Test Plan Version**: 1.0
**Last Updated**: 2025-12-22
**Status**: Ready for Implementation