-
Notifications
You must be signed in to change notification settings - Fork 0
test: implement tenant-aware temporal role tests (Issue #110) #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add migration for temporal columns to model_has_roles table * valid_from, valid_until for time-limited assignments * auto_revoke flag for automatic expiration * assigned_by, reason for audit trail * Indexed for efficient expiration queries - Create TemporalRoleUser pivot model * active() scope filters only current roles * expired() scope finds roles ready for revocation * isActive(), isExpired() helper methods * assignedBy() relationship - Override User::roles() to use custom pivot * Automatically filters inactive roles * Includes temporal pivot columns * Maintains Spatie compatibility - Add comprehensive unit tests (12 test cases) * Temporal filtering (future/expired/active) * Scopes (active, expired) * Helper methods (isActive, isExpired) * auto_revoke flag behavior Relates to #5 (RBAC System) Phase 1 of 4 - Foundation complete
- Changed $casts property to casts() method (Laravel 11+ convention) - Fixed missing newline before PHPDoc (line 108) - Refactored temporal filtering to DRY principle: - Extracted shared logic to applyActiveFilter() method - Eliminated code duplication between TemporalRoleUser::scopeActive() and User::roles() - Added @template TModel for PHPStan Level Max compliance - Updated CHANGELOG.md with RBAC Phase 1 entry (Added section) Addresses 3 Copilot review comments from PR #109 PHPStan Level Max: 0 errors ✓ Laravel Pint: Pass ✓
CRITICAL FIX: Spatie Permission teams require tenant_id in withPivot() for attach() to work correctly with multi-tenancy. Without this, all role assignments fail with NOT NULL constraint violation. Part of: #105
💡 Tip: Consider Using Draft PRsBenefits of opening PRs as drafts initially:
How to convert:
This is just a friendly reminder - feel free to continue as is! 😊 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements RBAC Phase 1: Temporal Role Foundation, introducing time-based role assignments with automatic expiration capabilities. The implementation extends Spatie Permission's role system with temporal validity periods, enabling roles to be active only within specified time windows.
Key Changes:
- Custom
TemporalRoleUserpivot model with temporal filtering logic - Database migration adding temporal columns to
model_has_rolestable - Comprehensive test suite with 12 test cases covering temporal behavior
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
app/Models/TemporalRoleUser.php |
New custom MorphPivot model with temporal scopes (active(), expired()) and helper methods (isActive(), isExpired()) |
app/Models/User.php |
Override of roles() relationship to use TemporalRoleUser pivot with automatic temporal filtering |
database/migrations/2025_11_08_143609_add_temporal_columns_to_model_has_roles_table.php |
Migration adding temporal columns (valid_from, valid_until, auto_revoke) and audit trail fields (assigned_by, reason) |
tests/Pest.php |
New assignTemporalRole() helper function for test data setup |
tests/Feature/Models/TemporalRoleUserTest.php |
Comprehensive test suite covering temporal filtering, query scopes, helper methods, and auto-revoke logic |
CHANGELOG.md |
Documentation of new features and changes |
💡 Tip: Consider Using Draft PRsBenefits of opening PRs as drafts initially:
How to convert:
This is just a friendly reminder - feel free to continue as is! 😊 |
Implements 12 comprehensive test cases for TemporalRoleUser pivot model to resolve TDD compliance requirement blocking PR #109. Test Coverage: - Temporal Filtering (5 tests) - Query Scopes (2 tests) - Helper Methods (4 tests) - Auto-Revoke (1 test) Technical Implementation: - Uses RefreshDatabase trait for test isolation - Proper tenant context via setPermissionsTeamId() - UUID handling for assigned_by foreign key - Direct DB queries to test pivot table logic - Tests work WITHOUT modifications to production code Resolves #110 Part of: #105 (RBAC Phase 1) Blocks: #109 (PR awaiting these tests)
…arity) - CHANGELOG.md: Remove duplicate 'German translations' entry (line 32) - TemporalRoleUserTest.php: Clarify timestamp comparison comment (second precision, not 'ignore microseconds') Copilot Review: 2 comments addressed
d49e573 to
a43f4f0
Compare
💡 Tip: Consider Using Draft PRsBenefits of opening PRs as drafts initially:
How to convert:
This is just a friendly reminder - feel free to continue as is! 😊 |
💡 Tip: Consider Using Draft PRsBenefits of opening PRs as drafts initially:
How to convert:
This is just a friendly reminder - feel free to continue as is! 😊 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
| 'role_id', | ||
| 'model_type', | ||
| 'model_id', | ||
| 'team_id', |
Copilot
AI
Nov 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fillable array includes 'team_id' but the Spatie Permission package uses 'tenant_id' as the team foreign key (as configured in config/permission.php line 102: 'team_foreign_key' => 'tenant_id'). This should be 'tenant_id' to match the actual column name used throughout the codebase.
| 'team_id', | |
| 'tenant_id', |
| ->where(function ($query) { | ||
| // Only return currently active roles using shared filtering logic | ||
| TemporalRoleUser::applyActiveFilter($query, 'model_has_roles.'); | ||
| }); |
Copilot
AI
Nov 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The where callback parameter $query lacks type hints. Adding a type hint Builder $query would improve type safety and IDE support, consistent with the type hints used in TemporalRoleUser::applyActiveFilter().
Implements RBAC Phase 1 (Issue #105) - temporal role assignments with automatic expiration. This PR provides the foundation for time-limited role assignments with tenant isolation, preparing for future manual/scheduled revocation workflows (Phase 2-4). ## Changes ### Core Implementation - **TemporalRoleUser**: Custom MorphPivot model for model_has_roles table - Temporal validity: valid_from, valid_until, auto_revoke columns - Query scopes: active(), expired() for time-based filtering - Helper methods: isActive(), isExpired() for pivot state checks - Audit trail: assigned_by (UUID FK), reason columns - **User Model**: Override roles() relationship - Automatic temporal filtering via applyActiveFilter() - Added tenant_id to withPivot() for Spatie Permission teams support - **Migration**: Add temporal columns to model_has_roles - TIMESTAMPTZ with proper timezone support - UUID foreign key to users.id for assigned_by - Composite index on (valid_until, auto_revoke) for expired() queries ### Code Quality - DRY: Shared applyActiveFilter() method with Template Generics - Laravel 11+: casts() method (not property) - PSR-12: Proper PHPDoc separation - PHPStan Level Max: 0 errors ### Testing (via PR #112) All 12 test cases passing (163 total, 467 assertions): - ✅ Temporal filtering (future/expired/active/permanent roles) - ✅ Query scopes (active(), expired()) - ✅ Helper methods (isActive(), isExpired()) - ✅ Auto-revoke flag behavior - ✅ Proper tenant isolation ## Architecture See ADR-004 for full decision record and Phase 2-4 roadmap. Relates to #105 (RBAC Phase 1) Part of #5 (RBAC parent epic)
Addresses post-merge Copilot feedback on RBAC Phase 1 code quality: 1. TemporalRoleUser.php: - Fix: Changed $fillable array from 'team_id' to 'tenant_id' - Reason: Matches Spatie Permission config (team_foreign_key => 'tenant_id') - Impact: Ensures mass assignment uses correct column name 2. User.php: - Fix: Added type hint 'Builder $query' to where() callback - Reason: Improves type safety, IDE support, consistency with applyActiveFilter() - Impact: Better developer experience, aligns with PHPStan Level Max All tests passing (12/12 temporal role tests, 163 total)
Addresses post-merge Copilot feedback on RBAC Phase 1 code quality: 1. TemporalRoleUser.php: - Fix: Changed $fillable array from 'team_id' to 'tenant_id' - Reason: Matches Spatie Permission config (team_foreign_key => 'tenant_id') - Impact: Ensures mass assignment uses correct column name 2. User.php: - Fix: Added type hint 'Builder $query' to where() callback - Reason: Improves type safety, IDE support, consistency with applyActiveFilter() - Impact: Better developer experience, aligns with PHPStan Level Max All tests passing (163/163, 467 assertions). PHPStan Level Max: 0 errors. Follow-up to #112
Implement Tenant-Aware Temporal Role Tests
Context
PR #109 (RBAC Phase 1) implements the temporal role foundation but did not include tests due to architectural constraints discovered during implementation.
Changes
Test Infrastructure (
tests/Pest.php)assignTemporalRole()- Directly inserts temporal role assignments with tenant_id supportattach()Test Suite (
tests/Feature/Models/TemporalRoleUserTest.php)Implements all 12 test cases from Issue #110:
Temporal Filtering (5 tests)
Query Scopes (2 tests)
active()scope returns only active rolesexpired()scope returns only expired roles (with auto_revoke=true)Helper Methods (4 tests)
isActive()correctly identifies active assignmentisActive()correctly identifies inactive assignment (future)isExpired()correctly identifies expired assignmentisExpired()correctly identifies non-expired assignmentAuto-Revoke (1 test)
auto_revokeflag (only auto_revoke=true roles appear in expired() scope)CHANGELOG
Test Results
All 12 new tests pass with proper tenant isolation and temporal filtering.
Architecture Notes
Multi-Tenancy Pattern
TenantKey(for encryption) as tenant identifier'teams' => true,'team_foreign_key' => 'tenant_id'model_has_roles(Spatie-managed)PermissionRegistrar::setPermissionsTeamId($tenantId)before role operationsWhy Direct DB Insert?
User::roles() relationship has
->where()constraints for temporal filtering, which interfere withattach()operations. TheassignTemporalRole()helper bypasses this by:model_has_rolestableunsetRelation('roles')Acceptance Criteria
Dependencies
Fixes #110