Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
U
UGC Heaven Web PHP
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
UGC Heaven Web PHP
Commits
17007ee0
Commit
17007ee0
authored
Jun 27, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
cool
parent
a5e223fe
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
4973 additions
and
0 deletions
+4973
-0
DeliverablePolicyTest.php
tests/Feature/Deliverables/DeliverablePolicyTest.php
+199
-0
InvitationPolicyTest.php
tests/Feature/Invitations/InvitationPolicyTest.php
+220
-0
MatchingServiceTest.php
tests/Feature/Matching/MatchingServiceTest.php
+592
-0
NotificationApiTest.php
tests/Feature/Notifications/NotificationApiTest.php
+413
-0
NotificationServiceTest.php
tests/Feature/Notifications/NotificationServiceTest.php
+1015
-0
ProjectPolicyTest.php
tests/Feature/Projects/ProjectPolicyTest.php
+354
-0
ReportServiceTest.php
tests/Feature/Reporting/ReportServiceTest.php
+785
-0
ReportingApiTest.php
tests/Feature/Reporting/ReportingApiTest.php
+549
-0
ReputationServiceTest.php
tests/Feature/Reputation/ReputationServiceTest.php
+464
-0
VideoReviewServiceTest.php
tests/Feature/VideoReview/VideoReviewServiceTest.php
+382
-0
No files found.
tests/Feature/Deliverables/DeliverablePolicyTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Deliverables
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Deliverables\Models\Deliverable
;
use
App\Modules\Projects\Models\Project
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Tests\TestCase
;
class
DeliverablePolicyTest
extends
TestCase
{
use
RefreshDatabase
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
User
$companyUser
;
private
CompanyProfile
$company
;
private
User
$otherCreatorUser
;
private
User
$otherCompanyUser
;
private
User
$admin
;
private
Project
$project
;
private
Deliverable
$deliverable
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
]);
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
();
$this
->
company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
]);
$this
->
otherCreatorUser
=
User
::
factory
()
->
creator
()
->
create
();
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCreatorUser
->
id
]);
$this
->
otherCompanyUser
=
User
::
factory
()
->
company
()
->
create
();
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCompanyUser
->
id
]);
$this
->
admin
=
User
::
factory
()
->
admin
()
->
create
();
$campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
]);
$this
->
project
=
Project
::
factory
()
->
create
([
'campaign_id'
=>
$campaign
->
id
,
'company_id'
=>
$this
->
company
->
id
,
'creator_id'
=>
$this
->
creator
->
id
,
'status'
=>
'in_progress'
,
]);
$this
->
deliverable
=
Deliverable
::
factory
()
->
create
([
'project_id'
=>
$this
->
project
->
id
,
'status'
=>
'pending'
,
]);
}
// ─── view ───────────────────────────────────────────────────────────
public
function
test_project_creator_can_view_deliverable
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'view'
,
$this
->
deliverable
));
}
public
function
test_project_company_can_view_deliverable
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'view'
,
$this
->
deliverable
));
}
public
function
test_admin_can_view_any_deliverable
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'view'
,
$this
->
deliverable
));
}
public
function
test_other_creator_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'view'
,
$this
->
deliverable
));
}
public
function
test_other_company_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'view'
,
$this
->
deliverable
));
}
// ─── submit ─────────────────────────────────────────────────────────
public
function
test_creator_can_submit_to_submittable_deliverable
()
:
void
{
$result
=
$this
->
creatorUser
->
can
(
'submit'
,
$this
->
deliverable
);
$this
->
assertEquals
(
$this
->
deliverable
->
canBeSubmittedTo
(),
$result
);
}
public
function
test_company_cannot_submit
()
:
void
{
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'submit'
,
$this
->
deliverable
));
}
public
function
test_other_creator_cannot_submit
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'submit'
,
$this
->
deliverable
));
}
public
function
test_admin_cannot_submit
()
:
void
{
$this
->
assertFalse
(
$this
->
admin
->
can
(
'submit'
,
$this
->
deliverable
));
}
// ─── approve ────────────────────────────────────────────────────────
public
function
test_company_can_approve_approvable_deliverable
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$result
=
$this
->
companyUser
->
can
(
'approve'
,
$this
->
deliverable
);
$this
->
assertEquals
(
$this
->
deliverable
->
canBeApproved
(),
$result
);
}
public
function
test_admin_can_approve
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$result
=
$this
->
admin
->
can
(
'approve'
,
$this
->
deliverable
);
$this
->
assertEquals
(
$this
->
deliverable
->
canBeApproved
(),
$result
);
}
public
function
test_creator_cannot_approve
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'approve'
,
$this
->
deliverable
));
}
public
function
test_other_company_cannot_approve
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'approve'
,
$this
->
deliverable
));
}
// ─── requestRevision ────────────────────────────────────────────────
public
function
test_company_can_request_revision_when_allowed
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$result
=
$this
->
companyUser
->
can
(
'requestRevision'
,
$this
->
deliverable
);
$this
->
assertEquals
(
$this
->
deliverable
->
canRequestRevision
(),
$result
);
}
public
function
test_admin_can_request_revision
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$result
=
$this
->
admin
->
can
(
'requestRevision'
,
$this
->
deliverable
);
$this
->
assertEquals
(
$this
->
deliverable
->
canRequestRevision
(),
$result
);
}
public
function
test_creator_cannot_request_revision
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'requestRevision'
,
$this
->
deliverable
));
}
public
function
test_other_company_cannot_request_revision
()
:
void
{
$this
->
deliverable
->
update
([
'status'
=>
'submitted'
]);
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'requestRevision'
,
$this
->
deliverable
));
}
// ─── manage ─────────────────────────────────────────────────────────
public
function
test_company_can_manage_own_deliverable
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'manage'
,
$this
->
deliverable
));
}
public
function
test_admin_can_manage_any_deliverable
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'manage'
,
$this
->
deliverable
));
}
public
function
test_creator_cannot_manage
()
:
void
{
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'manage'
,
$this
->
deliverable
));
}
public
function
test_other_company_cannot_manage
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'manage'
,
$this
->
deliverable
));
}
}
tests/Feature/Invitations/InvitationPolicyTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Invitations
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Invitations\Models\Invitation
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Tests\TestCase
;
class
InvitationPolicyTest
extends
TestCase
{
use
RefreshDatabase
;
private
User
$companyUser
;
private
CompanyProfile
$company
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
User
$otherCompanyUser
;
private
CompanyProfile
$otherCompany
;
private
User
$otherCreatorUser
;
private
CreatorProfile
$otherCreator
;
private
User
$admin
;
private
Campaign
$campaign
;
private
Invitation
$invitation
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
();
$this
->
company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
]);
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
]);
$this
->
otherCompanyUser
=
User
::
factory
()
->
company
()
->
create
();
$this
->
otherCompany
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCompanyUser
->
id
]);
$this
->
otherCreatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
otherCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCreatorUser
->
id
]);
$this
->
admin
=
User
::
factory
()
->
admin
()
->
create
();
$this
->
campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
]);
$this
->
invitation
=
Invitation
::
factory
()
->
create
([
'campaign_id'
=>
$this
->
campaign
->
id
,
'company_id'
=>
$this
->
company
->
id
,
'creator_id'
=>
$this
->
creator
->
id
,
'status'
=>
'sent'
,
'expires_at'
=>
now
()
->
addDays
(
7
),
'resend_count'
=>
0
,
]);
}
// ─── viewAny ────────────────────────────────────────────────────────
public
function
test_any_authenticated_user_can_view_any
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'viewAny'
,
Invitation
::
class
));
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'viewAny'
,
Invitation
::
class
));
$this
->
assertTrue
(
$this
->
admin
->
can
(
'viewAny'
,
Invitation
::
class
));
}
// ─── view ───────────────────────────────────────────────────────────
public
function
test_sending_company_can_view
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'view'
,
$this
->
invitation
));
}
public
function
test_invited_creator_can_view
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'view'
,
$this
->
invitation
));
}
public
function
test_admin_can_view_any_invitation
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'view'
,
$this
->
invitation
));
}
public
function
test_other_company_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'view'
,
$this
->
invitation
));
}
public
function
test_other_creator_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'view'
,
$this
->
invitation
));
}
// ─── create ─────────────────────────────────────────────────────────
public
function
test_company_with_profile_can_create
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'create'
,
Invitation
::
class
));
}
public
function
test_creator_cannot_create
()
:
void
{
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'create'
,
Invitation
::
class
));
}
public
function
test_admin_cannot_create
()
:
void
{
$this
->
assertFalse
(
$this
->
admin
->
can
(
'create'
,
Invitation
::
class
));
}
// ─── cancel ─────────────────────────────────────────────────────────
public
function
test_sending_company_can_cancel_pending_invitation
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'cancel'
,
$this
->
invitation
));
}
public
function
test_sending_company_cannot_cancel_accepted_invitation
()
:
void
{
$this
->
invitation
->
update
([
'status'
=>
'accepted'
]);
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'cancel'
,
$this
->
invitation
));
}
public
function
test_other_company_cannot_cancel
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'cancel'
,
$this
->
invitation
));
}
public
function
test_creator_cannot_cancel
()
:
void
{
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'cancel'
,
$this
->
invitation
));
}
// ─── resend ─────────────────────────────────────────────────────────
public
function
test_sending_company_can_resend_when_allowed
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'resend'
,
$this
->
invitation
));
}
public
function
test_sending_company_cannot_resend_after_max_resends
()
:
void
{
$this
->
invitation
->
update
([
'resend_count'
=>
2
]);
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'resend'
,
$this
->
invitation
));
}
public
function
test_sending_company_cannot_resend_accepted
()
:
void
{
$this
->
invitation
->
update
([
'status'
=>
'accepted'
]);
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'resend'
,
$this
->
invitation
));
}
public
function
test_other_company_cannot_resend
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'resend'
,
$this
->
invitation
));
}
public
function
test_creator_cannot_resend
()
:
void
{
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'resend'
,
$this
->
invitation
));
}
// ─── accept ─────────────────────────────────────────────────────────
public
function
test_invited_creator_can_accept_pending
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'accept'
,
$this
->
invitation
));
}
public
function
test_invited_creator_cannot_accept_expired
()
:
void
{
$this
->
invitation
->
update
([
'expires_at'
=>
now
()
->
subDay
()]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'accept'
,
$this
->
invitation
));
}
public
function
test_invited_creator_cannot_accept_already_accepted
()
:
void
{
$this
->
invitation
->
update
([
'status'
=>
'accepted'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'accept'
,
$this
->
invitation
));
}
public
function
test_other_creator_cannot_accept
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'accept'
,
$this
->
invitation
));
}
public
function
test_company_cannot_accept
()
:
void
{
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'accept'
,
$this
->
invitation
));
}
// ─── decline ────────────────────────────────────────────────────────
public
function
test_invited_creator_can_decline_pending
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'decline'
,
$this
->
invitation
));
}
public
function
test_invited_creator_cannot_decline_already_accepted
()
:
void
{
$this
->
invitation
->
update
([
'status'
=>
'accepted'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'decline'
,
$this
->
invitation
));
}
public
function
test_other_creator_cannot_decline
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'decline'
,
$this
->
invitation
));
}
public
function
test_company_cannot_decline
()
:
void
{
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'decline'
,
$this
->
invitation
));
}
}
tests/Feature/Matching/MatchingServiceTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Matching
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Matching\Services\MatchingService
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Tests\TestCase
;
class
MatchingServiceTest
extends
TestCase
{
use
RefreshDatabase
;
private
MatchingService
$service
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
User
$companyUser
;
private
CompanyProfile
$company
;
private
Campaign
$campaign
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
service
=
app
(
MatchingService
::
class
);
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
]);
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
,
'content_niches'
=>
[
'fashion'
,
'beauty'
,
'lifestyle'
],
'languages'
=>
[
'en'
,
'ar'
],
'country'
=>
'EG'
,
'skills'
=>
[
'video_editing'
,
'photography'
,
'storytelling'
],
'experience_level'
=>
'intermediate'
,
'equipment'
=>
[
'camera_dslr'
,
'ring_light'
,
'microphone'
],
'availability_status'
=>
'available'
,
'reputation_score'
=>
70
,
'completion_percentage'
=>
80
,
]);
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
([
'status'
=>
'active'
]);
$this
->
company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
,
]);
$this
->
campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
,
'status'
=>
'published'
,
'niches'
=>
[
'fashion'
,
'beauty'
],
'languages'
=>
[
'en'
],
'countries'
=>
[
'EG'
,
'SA'
],
'specific_skills'
=>
[
'video_editing'
,
'photography'
],
'experience_level_min'
=>
'intermediate'
,
'equipment_requirements'
=>
[
'camera_dslr'
],
]);
}
// ─── Score Creator for Campaign ─────────────────────────────────────
public
function
test_score_returns_integer
()
:
void
{
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertIsInt
(
$score
);
}
public
function
test_score_is_between_0_and_100
()
:
void
{
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThanOrEqual
(
0
,
$score
);
$this
->
assertLessThanOrEqual
(
100
,
$score
);
}
public
function
test_perfect_match_gives_high_score
()
:
void
{
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
70
,
$score
);
}
public
function
test_no_match_gives_low_score
()
:
void
{
$mismatchCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
])
->
id
,
'content_niches'
=>
[
'gaming'
,
'tech'
],
'languages'
=>
[
'ja'
],
'country'
=>
'JP'
,
'skills'
=>
[
'game_streaming'
],
'experience_level'
=>
'beginner'
,
'equipment'
=>
[],
'availability_status'
=>
'unavailable'
,
'reputation_score'
=>
10
,
'completion_percentage'
=>
80
,
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$mismatchCreator
,
$this
->
campaign
);
$this
->
assertLessThan
(
30
,
$score
);
}
// ─── Niche Scoring ──────────────────────────────────────────────────
public
function
test_full_niche_overlap_gives_max_niche_score
()
:
void
{
$this
->
creator
->
update
([
'content_niches'
=>
[
'fashion'
,
'beauty'
,
'food'
]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
public
function
test_partial_niche_overlap_gives_partial_score
()
:
void
{
$this
->
creator
->
update
([
'content_niches'
=>
[
'fashion'
,
'tech'
]]);
$score1
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'content_niches'
=>
[
'fashion'
,
'beauty'
]]);
$score2
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
$score1
,
$score2
);
}
public
function
test_no_niche_overlap_gives_zero_niche_contribution
()
:
void
{
$this
->
creator
->
update
([
'content_niches'
=>
[
'gaming'
,
'tech'
,
'science'
]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$fullMatchCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
])
->
id
,
'content_niches'
=>
[
'fashion'
,
'beauty'
],
'languages'
=>
$this
->
creator
->
languages
,
'country'
=>
$this
->
creator
->
country
,
'skills'
=>
$this
->
creator
->
skills
,
'experience_level'
=>
$this
->
creator
->
experience_level
,
'equipment'
=>
$this
->
creator
->
equipment
,
'availability_status'
=>
$this
->
creator
->
availability_status
,
'reputation_score'
=>
$this
->
creator
->
reputation_score
,
'completion_percentage'
=>
80
,
]);
$fullScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$fullMatchCreator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$fullScore
,
$score
);
}
public
function
test_empty_campaign_niches_gives_full_niche_score
()
:
void
{
$this
->
campaign
->
update
([
'niches'
=>
[]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
public
function
test_empty_creator_niches_gives_zero_niche_score
()
:
void
{
$this
->
creator
->
update
([
'content_niches'
=>
[]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'content_niches'
=>
[
'fashion'
,
'beauty'
]]);
$withNiches
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$withNiches
,
$score
);
}
// ─── Language Scoring ───────────────────────────────────────────────
public
function
test_matching_language_gives_full_language_score
()
:
void
{
$this
->
creator
->
update
([
'languages'
=>
[
'en'
,
'ar'
]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
public
function
test_no_matching_language_gives_zero_language_score
()
:
void
{
$this
->
creator
->
update
([
'languages'
=>
[
'fr'
,
'de'
]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'languages'
=>
[
'en'
]]);
$withMatch
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$withMatch
,
$score
);
}
public
function
test_empty_campaign_languages_gives_full_language_score
()
:
void
{
$this
->
campaign
->
update
([
'languages'
=>
[]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
// ─── Country Scoring ────────────────────────────────────────────────
public
function
test_matching_country_gives_full_country_score
()
:
void
{
$this
->
creator
->
update
([
'country'
=>
'EG'
]);
$this
->
campaign
->
update
([
'countries'
=>
[
'EG'
,
'SA'
]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
public
function
test_non_matching_country_gives_zero_country_score
()
:
void
{
$this
->
creator
->
update
([
'country'
=>
'US'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'country'
=>
'EG'
]);
$withMatch
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$withMatch
,
$score
);
}
public
function
test_empty_campaign_countries_gives_full_country_score
()
:
void
{
$this
->
campaign
->
update
([
'countries'
=>
[]]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
// ─── Experience Scoring ─────────────────────────────────────────────
public
function
test_meets_experience_requirement_gives_full_score
()
:
void
{
$this
->
creator
->
update
([
'experience_level'
=>
'advanced'
]);
$this
->
campaign
->
update
([
'experience_level_min'
=>
'intermediate'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
60
,
$score
);
}
public
function
test_one_level_below_gives_partial_score
()
:
void
{
$this
->
creator
->
update
([
'experience_level'
=>
'beginner'
]);
$this
->
campaign
->
update
([
'experience_level_min'
=>
'intermediate'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'experience_level'
=>
'intermediate'
]);
$meetsReq
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$meetsReq
,
$score
);
}
public
function
test_no_experience_requirement_gives_full_score
()
:
void
{
$this
->
campaign
->
update
([
'experience_level_min'
=>
null
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
50
,
$score
);
}
// ─── Availability Scoring ───────────────────────────────────────────
public
function
test_available_gives_full_availability_score
()
:
void
{
$this
->
creator
->
update
([
'availability_status'
=>
'available'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'availability_status'
=>
'unavailable'
]);
$unavailScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
$unavailScore
,
$score
);
}
public
function
test_unavailable_gives_zero_availability_score
()
:
void
{
$this
->
creator
->
update
([
'availability_status'
=>
'unavailable'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'availability_status'
=>
'available'
]);
$availScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertLessThan
(
$availScore
,
$score
);
}
public
function
test_limited_availability_gives_partial_score
()
:
void
{
$this
->
creator
->
update
([
'availability_status'
=>
'limited'
]);
$score
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'availability_status'
=>
'available'
]);
$fullScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'availability_status'
=>
'unavailable'
]);
$zeroScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
$zeroScore
,
$score
);
$this
->
assertLessThan
(
$fullScore
,
$score
);
}
// ─── Reputation Scoring ─────────────────────────────────────────────
public
function
test_high_reputation_gives_high_reputation_contribution
()
:
void
{
$this
->
creator
->
update
([
'reputation_score'
=>
90
]);
$highRepScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
creator
->
update
([
'reputation_score'
=>
10
]);
$lowRepScore
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$this
->
assertGreaterThan
(
$lowRepScore
,
$highRepScore
);
}
// ─── Score Symmetry ─────────────────────────────────────────────────
public
function
test_score_campaign_for_creator_equals_reverse
()
:
void
{
$scoreA
=
$this
->
service
->
scoreCreatorForCampaign
(
$this
->
creator
,
$this
->
campaign
);
$scoreB
=
$this
->
service
->
scoreCampaignForCreator
(
$this
->
campaign
,
$this
->
creator
);
$this
->
assertEquals
(
$scoreA
,
$scoreB
);
}
// ─── Recommended Campaigns ──────────────────────────────────────────
public
function
test_get_recommended_campaigns_returns_collection
()
:
void
{
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
$this
->
assertInstanceOf
(
\Illuminate\Support\Collection
::
class
,
$result
);
}
public
function
test_get_recommended_campaigns_excludes_already_applied
()
:
void
{
$this
->
campaign
->
applications
()
->
create
([
'creator_id'
=>
$this
->
creator
->
id
,
'status'
=>
'pending'
,
'cover_letter'
=>
'I want to apply.'
,
]);
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
campaign
->
id
));
}
public
function
test_get_recommended_campaigns_excludes_past_deadline
()
:
void
{
$this
->
campaign
->
update
([
'application_deadline'
=>
now
()
->
subDay
()]);
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
campaign
->
id
));
}
public
function
test_get_recommended_campaigns_includes_no_deadline
()
:
void
{
$this
->
campaign
->
update
([
'application_deadline'
=>
null
]);
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
$this
->
assertTrue
(
$result
->
contains
(
'id'
,
$this
->
campaign
->
id
));
}
public
function
test_get_recommended_campaigns_only_published
()
:
void
{
$this
->
campaign
->
update
([
'status'
=>
'draft'
]);
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
campaign
->
id
));
}
public
function
test_get_recommended_campaigns_respects_limit
()
:
void
{
for
(
$i
=
0
;
$i
<
15
;
$i
++
)
{
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
,
'status'
=>
'published'
,
'niches'
=>
[
'fashion'
],
'languages'
=>
[
'en'
],
'countries'
=>
[
'EG'
],
]);
}
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
,
5
);
$this
->
assertLessThanOrEqual
(
5
,
$result
->
count
());
}
public
function
test_get_recommended_campaigns_sorted_by_score_desc
()
:
void
{
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
,
'status'
=>
'published'
,
'niches'
=>
[
'fashion'
,
'beauty'
],
'languages'
=>
[
'en'
],
'countries'
=>
[
'EG'
],
]);
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
,
'status'
=>
'published'
,
'niches'
=>
[
'gaming'
],
'languages'
=>
[
'ja'
],
'countries'
=>
[
'JP'
],
]);
$result
=
$this
->
service
->
getRecommendedCampaigns
(
$this
->
creator
);
if
(
$result
->
count
()
>=
2
)
{
$scores
=
$result
->
pluck
(
'match_score'
)
->
toArray
();
$sorted
=
$scores
;
rsort
(
$sorted
);
$this
->
assertEquals
(
$sorted
,
$scores
);
}
}
// ─── Recommended Creators ───────────────────────────────────────────
public
function
test_get_recommended_creators_returns_collection
()
:
void
{
$result
=
$this
->
service
->
getRecommendedCreators
(
$this
->
campaign
);
$this
->
assertInstanceOf
(
\Illuminate\Support\Collection
::
class
,
$result
);
}
public
function
test_get_recommended_creators_excludes_already_applied
()
:
void
{
$this
->
campaign
->
applications
()
->
create
([
'creator_id'
=>
$this
->
creator
->
id
,
'status'
=>
'pending'
,
'cover_letter'
=>
'Applied already.'
,
]);
$result
=
$this
->
service
->
getRecommendedCreators
(
$this
->
campaign
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
creator
->
id
));
}
public
function
test_get_recommended_creators_only_active_users
()
:
void
{
$this
->
creatorUser
->
update
([
'status'
=>
'suspended'
]);
$result
=
$this
->
service
->
getRecommendedCreators
(
$this
->
campaign
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
creator
->
id
));
}
public
function
test_get_recommended_creators_requires_50_percent_profile
()
:
void
{
$this
->
creator
->
update
([
'completion_percentage'
=>
30
]);
$result
=
$this
->
service
->
getRecommendedCreators
(
$this
->
campaign
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
creator
->
id
));
}
public
function
test_get_recommended_creators_respects_limit
()
:
void
{
for
(
$i
=
0
;
$i
<
25
;
$i
++
)
{
$user
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$user
->
id
,
'content_niches'
=>
[
'fashion'
],
'languages'
=>
[
'en'
],
'country'
=>
'EG'
,
'completion_percentage'
=>
80
,
'reputation_score'
=>
50
,
'availability_status'
=>
'available'
,
]);
}
$result
=
$this
->
service
->
getRecommendedCreators
(
$this
->
campaign
,
10
);
$this
->
assertLessThanOrEqual
(
10
,
$result
->
count
());
}
// ─── Similar Creators ───────────────────────────────────────────────
public
function
test_get_similar_creators_excludes_self
()
:
void
{
$result
=
$this
->
service
->
getSimilarCreators
(
$this
->
creator
);
$this
->
assertFalse
(
$result
->
contains
(
'id'
,
$this
->
creator
->
id
));
}
public
function
test_get_similar_creators_returns_similar_profiles
()
:
void
{
$similarUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$similarUser
->
id
,
'content_niches'
=>
[
'fashion'
,
'beauty'
,
'travel'
],
'skills'
=>
[
'video_editing'
,
'photography'
],
'country'
=>
'EG'
,
'experience_level'
=>
'intermediate'
,
'completion_percentage'
=>
80
,
]);
$result
=
$this
->
service
->
getSimilarCreators
(
$this
->
creator
);
$this
->
assertTrue
(
$result
->
count
()
>=
1
);
}
public
function
test_get_similar_creators_excludes_dissimilar
()
:
void
{
$differentUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$differentUser
->
id
,
'content_niches'
=>
[
'gaming'
,
'tech'
],
'skills'
=>
[
'game_streaming'
,
'coding'
],
'country'
=>
'JP'
,
'experience_level'
=>
'expert'
,
'completion_percentage'
=>
80
,
]);
$result
=
$this
->
service
->
getSimilarCreators
(
$this
->
creator
);
$ids
=
$result
->
pluck
(
'id'
)
->
toArray
();
$differentCreator
=
CreatorProfile
::
where
(
'user_id'
,
$differentUser
->
id
)
->
first
();
if
(
$differentCreator
)
{
$this
->
assertNotContains
(
$differentCreator
->
id
,
$ids
);
}
}
public
function
test_get_similar_creators_only_active_users
()
:
void
{
$inactiveUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'suspended'
]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$inactiveUser
->
id
,
'content_niches'
=>
[
'fashion'
,
'beauty'
],
'skills'
=>
[
'video_editing'
,
'photography'
],
'country'
=>
'EG'
,
'experience_level'
=>
'intermediate'
,
'completion_percentage'
=>
80
,
]);
$result
=
$this
->
service
->
getSimilarCreators
(
$this
->
creator
);
$ids
=
$result
->
pluck
(
'user_id'
)
->
toArray
();
$this
->
assertNotContains
(
$inactiveUser
->
id
,
$ids
);
}
public
function
test_get_similar_creators_respects_limit
()
:
void
{
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
{
$user
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$user
->
id
,
'content_niches'
=>
[
'fashion'
,
'beauty'
],
'skills'
=>
[
'video_editing'
,
'photography'
],
'country'
=>
'EG'
,
'experience_level'
=>
'intermediate'
,
'completion_percentage'
=>
80
,
]);
}
$result
=
$this
->
service
->
getSimilarCreators
(
$this
->
creator
,
3
);
$this
->
assertLessThanOrEqual
(
3
,
$result
->
count
());
}
// ─── Weight Totals ──────────────────────────────────────────────────
public
function
test_weights_sum_to_100
()
:
void
{
$reflection
=
new
\ReflectionClass
(
MatchingService
::
class
);
$weights
=
[];
foreach
(
$reflection
->
getConstants
()
as
$name
=>
$value
)
{
if
(
str_starts_with
(
$name
,
'WEIGHT_'
))
{
$weights
[
$name
]
=
$value
;
}
}
$this
->
assertEquals
(
100
,
array_sum
(
$weights
));
}
}
tests/Feature/Notifications/NotificationApiTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Notifications
;
use
App\Models\User
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Notifications\Enums\NotificationCategory
;
use
App\Modules\Notifications\Models\Notification
;
use
App\Modules\Notifications\Models\NotificationPreference
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Illuminate\Support\Facades\Cache
;
use
Tests\TestCase
;
class
NotificationApiTest
extends
TestCase
{
use
RefreshDatabase
;
private
User
$user
;
private
User
$otherUser
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
user
=
User
::
factory
()
->
creator
()
->
create
([
'email_verified_at'
=>
now
()]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
user
->
id
]);
$this
->
otherUser
=
User
::
factory
()
->
creator
()
->
create
([
'email_verified_at'
=>
now
()]);
}
// ─── Notification Index (GET /notifications) ────────────────────────
public
function
test_authenticated_user_can_list_notifications
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test notification'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonStructure
([
'success'
,
'data'
,
'meta'
=>
[
'current_page'
,
'per_page'
,
'total'
,
'unread_count'
],
]);
}
public
function
test_unauthenticated_user_cannot_list_notifications
()
:
void
{
$response
=
$this
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
401
);
}
public
function
test_notification_index_only_shows_own_notifications
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Mine'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
otherUser
->
id
,
'type'
=>
'test'
,
'title'
=>
'Not mine'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonCount
(
1
,
'data'
);
}
public
function
test_notification_index_filters_by_category
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'System'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Project'
,
'category'
=>
NotificationCategory
::
Projects
,
'icon'
=>
'briefcase'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications?category=system'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonCount
(
1
,
'data'
);
}
public
function
test_notification_index_includes_unread_count_in_meta
()
:
void
{
for
(
$i
=
0
;
$i
<
3
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Unread
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
Cache
::
flush
();
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonPath
(
'meta.unread_count'
,
3
);
}
public
function
test_notification_index_paginates_at_25
()
:
void
{
for
(
$i
=
0
;
$i
<
30
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Notification
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonPath
(
'meta.per_page'
,
25
);
$response
->
assertJsonPath
(
'meta.total'
,
30
);
}
public
function
test_notification_index_returns_proper_structure
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'application_accepted'
,
'title'
=>
'Application Accepted'
,
'body'
=>
'Your application has been accepted.'
,
'action_url'
=>
'/projects/123'
,
'action_label'
=>
'View Project'
,
'category'
=>
NotificationCategory
::
Applications
,
'icon'
=>
'check-circle'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonStructure
([
'data'
=>
[
'*'
=>
[
'id'
,
'type'
,
'title'
,
'body'
,
'icon'
,
'category'
,
'action_url'
,
'action_label'
,
'is_read'
,
'time_ago'
,
'created_at'
,
],
],
]);
}
// ─── Dropdown (GET /notifications/dropdown) ─────────────────────────
public
function
test_dropdown_returns_latest_10
()
:
void
{
for
(
$i
=
0
;
$i
<
15
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Notification
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications/dropdown'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonCount
(
10
,
'data.notifications'
);
}
public
function
test_dropdown_includes_unread_count
()
:
void
{
for
(
$i
=
0
;
$i
<
3
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Unread
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
Cache
::
flush
();
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications/dropdown'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonPath
(
'data.unread_count'
,
3
);
}
// ─── Unread Count (GET /notifications/unread-count) ─────────────────
public
function
test_unread_count_endpoint_returns_count
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Unread'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Cache
::
flush
();
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications/unread-count'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonPath
(
'data.count'
,
1
);
}
// ─── Mark Read (POST /notifications/{notification}/read) ────────────
public
function
test_user_can_mark_own_notification_as_read
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
postJson
(
"/notifications/
{
$notification
->
uuid
}
/read"
);
$response
->
assertStatus
(
200
);
$notification
->
refresh
();
$this
->
assertTrue
(
$notification
->
is_read
);
$this
->
assertNotNull
(
$notification
->
read_at
);
}
public
function
test_user_cannot_mark_other_users_notification_as_read
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
otherUser
->
id
,
'type'
=>
'test'
,
'title'
=>
'Not mine'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$response
=
$this
->
actingAs
(
$this
->
user
)
->
postJson
(
"/notifications/
{
$notification
->
uuid
}
/read"
);
$response
->
assertStatus
(
403
);
}
// ─── Mark All Read (POST /notifications/mark-all-read) ──────────────
public
function
test_user_can_mark_all_as_read
()
:
void
{
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Notification
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$response
=
$this
->
actingAs
(
$this
->
user
)
->
postJson
(
'/notifications/mark-all-read'
);
$response
->
assertStatus
(
200
);
$unread
=
Notification
::
forUser
(
$this
->
user
->
id
)
->
unread
()
->
count
();
$this
->
assertEquals
(
0
,
$unread
);
}
public
function
test_mark_all_read_does_not_affect_other_users
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
otherUser
->
id
,
'type'
=>
'test'
,
'title'
=>
'Other user'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$this
->
actingAs
(
$this
->
user
)
->
postJson
(
'/notifications/mark-all-read'
);
$unread
=
Notification
::
forUser
(
$this
->
otherUser
->
id
)
->
unread
()
->
count
();
$this
->
assertEquals
(
1
,
$unread
);
}
// ─── Preferences (GET /notifications/settings) ──────────────────────
public
function
test_user_can_view_notification_preferences
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
user
)
->
get
(
'/notifications/settings'
);
$response
->
assertStatus
(
200
);
}
public
function
test_preferences_api_returns_all_categories
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
user
)
->
getJson
(
'/notifications/settings'
);
$response
->
assertStatus
(
200
);
$response
->
assertJsonStructure
([
'success'
,
'data'
]);
}
// ─── Update Preferences (PUT /notifications/settings) ───────────────
public
function
test_user_can_update_preferences
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
user
)
->
putJson
(
'/notifications/settings'
,
[
'preferences'
=>
[
'applications'
=>
[
'in_app'
=>
true
,
'email'
=>
false
,
'digest'
=>
'daily'
],
'messages'
=>
[
'in_app'
=>
true
,
'email'
=>
true
,
'digest'
=>
'instant'
],
],
]);
$response
->
assertStatus
(
200
);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
'applications'
,
'email_enabled'
=>
false
,
'email_digest'
=>
'daily'
,
]);
}
public
function
test_update_preferences_enforces_locked_categories
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
user
)
->
putJson
(
'/notifications/settings'
,
[
'preferences'
=>
[
'system'
=>
[
'in_app'
=>
false
,
'email'
=>
false
,
'digest'
=>
'disabled'
],
],
]);
$response
->
assertStatus
(
200
);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
'system'
,
'email_enabled'
=>
true
,
]);
}
public
function
test_unauthenticated_user_cannot_update_preferences
()
:
void
{
$response
=
$this
->
putJson
(
'/notifications/settings'
,
[
'preferences'
=>
[
'applications'
=>
[
'in_app'
=>
true
,
'email'
=>
false
,
'digest'
=>
'daily'
],
],
]);
$response
->
assertStatus
(
401
);
}
}
tests/Feature/Notifications/NotificationServiceTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Notifications
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Notifications\Enums\NotificationCategory
;
use
App\Modules\Notifications\Jobs\SendNotificationEmail
;
use
App\Modules\Notifications\Models\Notification
;
use
App\Modules\Notifications\Models\NotificationPreference
;
use
App\Modules\Notifications\Services\NotificationService
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Illuminate\Support\Facades\Cache
;
use
Illuminate\Support\Facades\Queue
;
use
Tests\TestCase
;
class
NotificationServiceTest
extends
TestCase
{
use
RefreshDatabase
;
private
NotificationService
$service
;
private
User
$user
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
service
=
app
(
NotificationService
::
class
);
$this
->
user
=
User
::
factory
()
->
creator
()
->
create
();
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
user
->
id
]);
}
// ─── Send Notification ──────────────────────────────────────────────
public
function
test_send_creates_notification_with_all_fields
()
:
void
{
Queue
::
fake
();
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'application_accepted'
,
NotificationCategory
::
Applications
,
'Your application was accepted'
,
'Company X accepted your application for Campaign Y.'
,
'/projects/123'
,
'View Project'
,
'check-circle'
,
[
'campaign_id'
=>
123
],
);
$this
->
assertInstanceOf
(
Notification
::
class
,
$notification
);
$this
->
assertEquals
(
$this
->
user
->
id
,
$notification
->
user_id
);
$this
->
assertEquals
(
'application_accepted'
,
$notification
->
type
);
$this
->
assertEquals
(
'Your application was accepted'
,
$notification
->
title
);
$this
->
assertEquals
(
'Company X accepted your application for Campaign Y.'
,
$notification
->
body
);
$this
->
assertEquals
(
'/projects/123'
,
$notification
->
action_url
);
$this
->
assertEquals
(
'View Project'
,
$notification
->
action_label
);
$this
->
assertEquals
(
'check-circle'
,
$notification
->
icon
);
$this
->
assertEquals
(
NotificationCategory
::
Applications
,
$notification
->
category
);
$this
->
assertEquals
([
'campaign_id'
=>
123
],
$notification
->
data
);
$this
->
assertFalse
(
$notification
->
is_read
);
$this
->
assertNull
(
$notification
->
read_at
);
}
public
function
test_send_creates_notification_with_minimal_fields
()
:
void
{
Queue
::
fake
();
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'system_update'
,
NotificationCategory
::
System
,
'System maintenance scheduled'
,
);
$this
->
assertNotNull
(
$notification
);
$this
->
assertNull
(
$notification
->
body
);
$this
->
assertNull
(
$notification
->
action_url
);
$this
->
assertNull
(
$notification
->
action_label
);
$this
->
assertNull
(
$notification
->
data
);
}
public
function
test_send_uses_category_icon_when_no_icon_specified
()
:
void
{
Queue
::
fake
();
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'new_message'
,
NotificationCategory
::
Messages
,
'New message received'
,
);
$this
->
assertEquals
(
'message-square'
,
$notification
->
icon
);
}
public
function
test_send_uses_custom_icon_over_category_default
()
:
void
{
Queue
::
fake
();
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'new_message'
,
NotificationCategory
::
Messages
,
'New message received'
,
icon
:
'mail-open'
,
);
$this
->
assertEquals
(
'mail-open'
,
$notification
->
icon
);
}
public
function
test_send_generates_uuid
()
:
void
{
Queue
::
fake
();
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'test_notification'
,
NotificationCategory
::
System
,
'Test'
,
);
$this
->
assertNotNull
(
$notification
->
uuid
);
$this
->
assertMatchesRegularExpression
(
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/'
,
$notification
->
uuid
);
}
public
function
test_send_stores_related_model_reference
()
:
void
{
Queue
::
fake
();
$companyUser
=
User
::
factory
()
->
company
()
->
create
();
$company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$companyUser
->
id
]);
$campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$company
->
id
]);
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign available'
,
related
:
$campaign
,
);
$this
->
assertEquals
(
get_class
(
$campaign
),
$notification
->
related_type
);
$this
->
assertEquals
(
$campaign
->
id
,
$notification
->
related_id
);
}
// ─── Deduplication ──────────────────────────────────────────────────
public
function
test_send_deduplicates_within_window
()
:
void
{
Queue
::
fake
();
$companyUser
=
User
::
factory
()
->
company
()
->
create
();
$company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$companyUser
->
id
]);
$campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$company
->
id
]);
$first
=
$this
->
service
->
send
(
$this
->
user
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign available'
,
related
:
$campaign
,
);
$second
=
$this
->
service
->
send
(
$this
->
user
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign available'
,
related
:
$campaign
,
);
$this
->
assertNotNull
(
$first
);
$this
->
assertNull
(
$second
);
$this
->
assertDatabaseCount
(
'notifications'
,
1
);
}
public
function
test_send_does_not_deduplicate_without_related_model
()
:
void
{
Queue
::
fake
();
$first
=
$this
->
service
->
send
(
$this
->
user
,
'system_update'
,
NotificationCategory
::
System
,
'Maintenance 1'
,
);
$second
=
$this
->
service
->
send
(
$this
->
user
,
'system_update'
,
NotificationCategory
::
System
,
'Maintenance 2'
,
);
$this
->
assertNotNull
(
$first
);
$this
->
assertNotNull
(
$second
);
$this
->
assertDatabaseCount
(
'notifications'
,
2
);
}
public
function
test_send_allows_same_type_for_different_users
()
:
void
{
Queue
::
fake
();
$otherUser
=
User
::
factory
()
->
creator
()
->
create
();
$companyUser
=
User
::
factory
()
->
company
()
->
create
();
$company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$companyUser
->
id
]);
$campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$company
->
id
]);
$first
=
$this
->
service
->
send
(
$this
->
user
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign'
,
related
:
$campaign
,
);
$second
=
$this
->
service
->
send
(
$otherUser
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign'
,
related
:
$campaign
,
);
$this
->
assertNotNull
(
$first
);
$this
->
assertNotNull
(
$second
);
}
// ─── User Preferences Respect ──────────────────────────────────────
public
function
test_send_respects_in_app_disabled_preference
()
:
void
{
Queue
::
fake
();
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Campaigns
,
'in_app_enabled'
=>
false
,
'email_enabled'
=>
false
,
'email_digest'
=>
'disabled'
,
]);
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign'
,
);
$this
->
assertNull
(
$notification
);
$this
->
assertDatabaseCount
(
'notifications'
,
0
);
}
public
function
test_send_ignores_in_app_preference_for_always_on_categories
()
:
void
{
Queue
::
fake
();
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Projects
,
'in_app_enabled'
=>
false
,
'email_enabled'
=>
false
,
'email_digest'
=>
'disabled'
,
]);
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'project_created'
,
NotificationCategory
::
Projects
,
'Project created'
,
);
$this
->
assertNotNull
(
$notification
);
}
// ─── Email Sending ──────────────────────────────────────────────────
public
function
test_send_queues_email_when_enabled
()
:
void
{
Queue
::
fake
();
$this
->
service
->
send
(
$this
->
user
,
'application_accepted'
,
NotificationCategory
::
Applications
,
'Application accepted'
,
);
Queue
::
assertPushed
(
SendNotificationEmail
::
class
);
}
public
function
test_send_does_not_queue_email_when_email_disabled
()
:
void
{
Queue
::
fake
();
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
false
,
'email_digest'
=>
'disabled'
,
]);
$this
->
service
->
send
(
$this
->
user
,
'application_accepted'
,
NotificationCategory
::
Applications
,
'Application accepted'
,
);
Queue
::
assertNotPushed
(
SendNotificationEmail
::
class
);
}
public
function
test_send_does_not_queue_email_when_send_email_false
()
:
void
{
Queue
::
fake
();
$this
->
service
->
send
(
$this
->
user
,
'application_accepted'
,
NotificationCategory
::
Applications
,
'Application accepted'
,
sendEmail
:
false
,
);
Queue
::
assertNotPushed
(
SendNotificationEmail
::
class
);
}
public
function
test_send_always_emails_for_system_category
()
:
void
{
Queue
::
fake
();
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
System
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
false
,
'email_digest'
=>
'disabled'
,
]);
$this
->
service
->
send
(
$this
->
user
,
'system_alert'
,
NotificationCategory
::
System
,
'Critical system alert'
,
);
Queue
::
assertPushed
(
SendNotificationEmail
::
class
);
}
public
function
test_send_does_not_email_when_digest_not_instant
()
:
void
{
Queue
::
fake
();
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
true
,
'email_digest'
=>
'daily'
,
]);
$this
->
service
->
send
(
$this
->
user
,
'application_received'
,
NotificationCategory
::
Applications
,
'New application'
,
);
Queue
::
assertNotPushed
(
SendNotificationEmail
::
class
);
}
// ─── Email Rate Limiting ────────────────────────────────────────────
public
function
test_send_rate_limits_emails_per_hour
()
:
void
{
Queue
::
fake
();
$cacheKey
=
"notif:email_count:
{
$this
->
user
->
id
}
:"
.
now
()
->
format
(
'Y-m-d-H'
);
Cache
::
put
(
$cacheKey
,
10
,
3600
);
$this
->
service
->
send
(
$this
->
user
,
'application_accepted'
,
NotificationCategory
::
Applications
,
'Application accepted'
,
);
Queue
::
assertNotPushed
(
SendNotificationEmail
::
class
);
}
// ─── Batch Threshold ────────────────────────────────────────────────
public
function
test_send_batches_after_threshold
()
:
void
{
Queue
::
fake
();
$cacheKey
=
"notif:batch:
{
$this
->
user
->
id
}
:many_messages"
;
Cache
::
put
(
$cacheKey
,
5
,
600
);
$notification
=
$this
->
service
->
send
(
$this
->
user
,
'many_messages'
,
NotificationCategory
::
Messages
,
'New message'
,
);
$this
->
assertNotNull
(
$notification
);
Queue
::
assertNotPushed
(
SendNotificationEmail
::
class
);
}
// ─── Send Bulk ──────────────────────────────────────────────────────
public
function
test_send_bulk_sends_to_multiple_users
()
:
void
{
Queue
::
fake
();
$users
=
collect
([
$this
->
user
,
User
::
factory
()
->
creator
()
->
create
(),
User
::
factory
()
->
creator
()
->
create
(),
]);
$sent
=
$this
->
service
->
sendBulk
(
$users
,
'system_update'
,
NotificationCategory
::
System
,
'Platform update'
,
);
$this
->
assertEquals
(
3
,
$sent
);
$this
->
assertDatabaseCount
(
'notifications'
,
3
);
}
public
function
test_send_bulk_respects_individual_preferences
()
:
void
{
Queue
::
fake
();
$user2
=
User
::
factory
()
->
creator
()
->
create
();
NotificationPreference
::
create
([
'user_id'
=>
$user2
->
id
,
'category'
=>
NotificationCategory
::
Campaigns
,
'in_app_enabled'
=>
false
,
'email_enabled'
=>
false
,
'email_digest'
=>
'disabled'
,
]);
$users
=
collect
([
$this
->
user
,
$user2
]);
$sent
=
$this
->
service
->
sendBulk
(
$users
,
'new_campaign'
,
NotificationCategory
::
Campaigns
,
'New campaign available'
,
);
$this
->
assertEquals
(
1
,
$sent
);
}
// ─── Mark Read ──────────────────────────────────────────────────────
public
function
test_mark_read_updates_notification
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$this
->
service
->
markRead
(
$notification
);
$notification
->
refresh
();
$this
->
assertTrue
(
$notification
->
is_read
);
$this
->
assertNotNull
(
$notification
->
read_at
);
}
public
function
test_mark_read_does_not_double_mark
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
true
,
'read_at'
=>
now
()
->
subHour
(),
]);
$originalReadAt
=
$notification
->
read_at
;
$this
->
service
->
markRead
(
$notification
);
$notification
->
refresh
();
$this
->
assertEquals
(
$originalReadAt
->
timestamp
,
$notification
->
read_at
->
timestamp
);
}
// ─── Mark All Read ──────────────────────────────────────────────────
public
function
test_mark_all_read_marks_all_unread_for_user
()
:
void
{
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Test
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$this
->
service
->
markAllRead
(
$this
->
user
);
$unread
=
Notification
::
forUser
(
$this
->
user
->
id
)
->
unread
()
->
count
();
$this
->
assertEquals
(
0
,
$unread
);
}
public
function
test_mark_all_read_does_not_affect_other_users
()
:
void
{
$otherUser
=
User
::
factory
()
->
creator
()
->
create
();
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Mine'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$otherUser
->
id
,
'type'
=>
'test'
,
'title'
=>
'Theirs'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
$this
->
service
->
markAllRead
(
$this
->
user
);
$otherUnread
=
Notification
::
forUser
(
$otherUser
->
id
)
->
unread
()
->
count
();
$this
->
assertEquals
(
1
,
$otherUnread
);
}
// ─── Unread Count ───────────────────────────────────────────────────
public
function
test_get_unread_count_returns_correct_count
()
:
void
{
for
(
$i
=
0
;
$i
<
3
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Unread
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Read'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
true
,
'read_at'
=>
now
(),
]);
Cache
::
flush
();
$count
=
$this
->
service
->
getUnreadCount
(
$this
->
user
);
$this
->
assertEquals
(
3
,
$count
);
}
public
function
test_get_unread_count_caches_result
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Cache
::
flush
();
$this
->
service
->
getUnreadCount
(
$this
->
user
);
$this
->
assertTrue
(
Cache
::
has
(
"notifications:unread:
{
$this
->
user
->
id
}
"
));
}
// ─── Get Latest ─────────────────────────────────────────────────────
public
function
test_get_latest_returns_limited_results
()
:
void
{
for
(
$i
=
0
;
$i
<
15
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Notification
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$latest
=
$this
->
service
->
getLatest
(
$this
->
user
,
10
);
$this
->
assertCount
(
10
,
$latest
);
}
public
function
test_get_latest_orders_by_most_recent
()
:
void
{
$old
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Old'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
'created_at'
=>
now
()
->
subDays
(
2
),
]);
$new
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'New'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
'created_at'
=>
now
(),
]);
$latest
=
$this
->
service
->
getLatest
(
$this
->
user
,
10
);
$this
->
assertEquals
(
$new
->
id
,
$latest
->
first
()
->
id
);
}
// ─── Get Paginated ──────────────────────────────────────────────────
public
function
test_get_paginated_returns_paginator
()
:
void
{
for
(
$i
=
0
;
$i
<
30
;
$i
++
)
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
"Notification
{
$i
}
"
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
}
$result
=
$this
->
service
->
getPaginated
(
$this
->
user
);
$this
->
assertEquals
(
25
,
$result
->
perPage
());
$this
->
assertEquals
(
30
,
$result
->
total
());
}
public
function
test_get_paginated_filters_by_category
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'System'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Project'
,
'category'
=>
NotificationCategory
::
Projects
,
'icon'
=>
'briefcase'
,
'is_read'
=>
false
,
]);
$result
=
$this
->
service
->
getPaginated
(
$this
->
user
,
NotificationCategory
::
System
);
$this
->
assertEquals
(
1
,
$result
->
total
());
$this
->
assertEquals
(
'System'
,
$result
->
first
()
->
title
);
}
// ─── User Preferences ───────────────────────────────────────────────
public
function
test_get_user_preferences_returns_all_categories
()
:
void
{
$preferences
=
$this
->
service
->
getUserPreferences
(
$this
->
user
);
$this
->
assertCount
(
count
(
NotificationCategory
::
cases
()),
$preferences
);
}
public
function
test_get_user_preferences_uses_defaults_when_no_stored
()
:
void
{
$preferences
=
$this
->
service
->
getUserPreferences
(
$this
->
user
);
$this
->
assertTrue
(
$preferences
[
'applications'
][
'in_app'
]);
$this
->
assertTrue
(
$preferences
[
'applications'
][
'email'
]);
$this
->
assertEquals
(
'instant'
,
$preferences
[
'applications'
][
'digest'
]);
}
public
function
test_get_user_preferences_uses_stored_values
()
:
void
{
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
false
,
'email_digest'
=>
'daily'
,
]);
$preferences
=
$this
->
service
->
getUserPreferences
(
$this
->
user
);
$this
->
assertTrue
(
$preferences
[
'applications'
][
'in_app'
]);
$this
->
assertFalse
(
$preferences
[
'applications'
][
'email'
]);
$this
->
assertEquals
(
'daily'
,
$preferences
[
'applications'
][
'digest'
]);
}
public
function
test_get_user_preferences_marks_locked_categories
()
:
void
{
$preferences
=
$this
->
service
->
getUserPreferences
(
$this
->
user
);
$this
->
assertTrue
(
$preferences
[
'system'
][
'email_locked'
]);
$this
->
assertTrue
(
$preferences
[
'projects'
][
'in_app_locked'
]);
$this
->
assertFalse
(
$preferences
[
'campaigns'
][
'in_app_locked'
]);
}
// ─── Update Preferences ─────────────────────────────────────────────
public
function
test_update_preferences_creates_new_records
()
:
void
{
$this
->
service
->
updatePreferences
(
$this
->
user
,
[
'applications'
=>
[
'in_app'
=>
true
,
'email'
=>
false
,
'digest'
=>
'daily'
],
]);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
->
value
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
false
,
'email_digest'
=>
'daily'
,
]);
}
public
function
test_update_preferences_updates_existing_records
()
:
void
{
NotificationPreference
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
,
'in_app_enabled'
=>
true
,
'email_enabled'
=>
true
,
'email_digest'
=>
'instant'
,
]);
$this
->
service
->
updatePreferences
(
$this
->
user
,
[
'applications'
=>
[
'in_app'
=>
true
,
'email'
=>
false
,
'digest'
=>
'weekly'
],
]);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Applications
->
value
,
'email_enabled'
=>
false
,
'email_digest'
=>
'weekly'
,
]);
$this
->
assertDatabaseCount
(
'notification_preferences'
,
1
);
}
public
function
test_update_preferences_enforces_always_on_in_app
()
:
void
{
$this
->
service
->
updatePreferences
(
$this
->
user
,
[
'projects'
=>
[
'in_app'
=>
false
,
'email'
=>
true
,
'digest'
=>
'instant'
],
]);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
Projects
->
value
,
'in_app_enabled'
=>
true
,
]);
}
public
function
test_update_preferences_enforces_always_on_email
()
:
void
{
$this
->
service
->
updatePreferences
(
$this
->
user
,
[
'system'
=>
[
'in_app'
=>
true
,
'email'
=>
false
,
'digest'
=>
'disabled'
],
]);
$this
->
assertDatabaseHas
(
'notification_preferences'
,
[
'user_id'
=>
$this
->
user
->
id
,
'category'
=>
NotificationCategory
::
System
->
value
,
'email_enabled'
=>
true
,
]);
}
// ─── Delete Old Notifications ───────────────────────────────────────
public
function
test_delete_old_notifications_removes_read_older_than_threshold
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Old read'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
true
,
'read_at'
=>
now
()
->
subDays
(
100
),
'created_at'
=>
now
()
->
subDays
(
100
),
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Recent read'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
true
,
'read_at'
=>
now
()
->
subDays
(
10
),
'created_at'
=>
now
()
->
subDays
(
10
),
]);
$deleted
=
$this
->
service
->
deleteOldNotifications
(
90
);
$this
->
assertEquals
(
1
,
$deleted
);
$this
->
assertDatabaseCount
(
'notifications'
,
1
);
}
public
function
test_delete_old_notifications_keeps_unread
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Old unread'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
'created_at'
=>
now
()
->
subDays
(
100
),
]);
$deleted
=
$this
->
service
->
deleteOldNotifications
(
90
);
$this
->
assertEquals
(
0
,
$deleted
);
$this
->
assertDatabaseCount
(
'notifications'
,
1
);
}
// ─── Model Scopes ───────────────────────────────────────────────────
public
function
test_scope_unread_filters_correctly
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Unread'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Read'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
true
,
'read_at'
=>
now
(),
]);
$this
->
assertCount
(
1
,
Notification
::
unread
()
->
get
());
$this
->
assertCount
(
1
,
Notification
::
read
()
->
get
());
}
public
function
test_scope_by_category_filters_correctly
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'System'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Message'
,
'category'
=>
NotificationCategory
::
Messages
,
'icon'
=>
'message-square'
,
'is_read'
=>
false
,
]);
$result
=
Notification
::
byCategory
(
NotificationCategory
::
Messages
)
->
get
();
$this
->
assertCount
(
1
,
$result
);
$this
->
assertEquals
(
'Message'
,
$result
->
first
()
->
title
);
}
public
function
test_scope_of_type_filters_correctly
()
:
void
{
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'application_accepted'
,
'title'
=>
'Accepted'
,
'category'
=>
NotificationCategory
::
Applications
,
'icon'
=>
'check'
,
'is_read'
=>
false
,
]);
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'application_rejected'
,
'title'
=>
'Rejected'
,
'category'
=>
NotificationCategory
::
Applications
,
'icon'
=>
'x'
,
'is_read'
=>
false
,
]);
$result
=
Notification
::
ofType
(
'application_accepted'
)
->
get
();
$this
->
assertCount
(
1
,
$result
);
}
// ─── Model Accessors ────────────────────────────────────────────────
public
function
test_display_icon_returns_custom_icon_when_set
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'custom-icon'
,
'is_read'
=>
false
,
]);
$this
->
assertEquals
(
'custom-icon'
,
$notification
->
display_icon
);
}
public
function
test_display_icon_falls_back_to_category_icon
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
Messages
,
'icon'
=>
null
,
'is_read'
=>
false
,
]);
$this
->
assertEquals
(
'message-square'
,
$notification
->
display_icon
);
}
public
function
test_time_ago_accessor_returns_human_readable
()
:
void
{
$notification
=
Notification
::
create
([
'user_id'
=>
$this
->
user
->
id
,
'type'
=>
'test'
,
'title'
=>
'Test'
,
'category'
=>
NotificationCategory
::
System
,
'icon'
=>
'bell'
,
'is_read'
=>
false
,
'created_at'
=>
now
()
->
subMinutes
(
5
),
]);
$this
->
assertNotEmpty
(
$notification
->
time_ago
);
$this
->
assertIsString
(
$notification
->
time_ago
);
}
// ─── NotificationCategory Enum ──────────────────────────────────────
public
function
test_category_icons_are_all_defined
()
:
void
{
foreach
(
NotificationCategory
::
cases
()
as
$category
)
{
$this
->
assertNotEmpty
(
$category
->
icon
());
}
}
public
function
test_system_email_always_on
()
:
void
{
$this
->
assertTrue
(
NotificationCategory
::
System
->
emailAlwaysOn
());
$this
->
assertFalse
(
NotificationCategory
::
Applications
->
emailAlwaysOn
());
}
public
function
test_in_app_always_on_for_most_categories
()
:
void
{
$this
->
assertTrue
(
NotificationCategory
::
Projects
->
inAppAlwaysOn
());
$this
->
assertTrue
(
NotificationCategory
::
Messages
->
inAppAlwaysOn
());
$this
->
assertFalse
(
NotificationCategory
::
Campaigns
->
inAppAlwaysOn
());
$this
->
assertFalse
(
NotificationCategory
::
Marketing
->
inAppAlwaysOn
());
}
public
function
test_default_preferences_defined_for_all_categories
()
:
void
{
$defaults
=
NotificationCategory
::
defaultPreferences
();
foreach
(
NotificationCategory
::
cases
()
as
$category
)
{
$this
->
assertArrayHasKey
(
$category
->
value
,
$defaults
);
$this
->
assertArrayHasKey
(
'in_app'
,
$defaults
[
$category
->
value
]);
$this
->
assertArrayHasKey
(
'email'
,
$defaults
[
$category
->
value
]);
$this
->
assertArrayHasKey
(
'digest'
,
$defaults
[
$category
->
value
]);
}
}
}
tests/Feature/Projects/ProjectPolicyTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Projects
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Projects\Models\Project
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Tests\TestCase
;
class
ProjectPolicyTest
extends
TestCase
{
use
RefreshDatabase
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
User
$companyUser
;
private
CompanyProfile
$company
;
private
User
$otherCreatorUser
;
private
CreatorProfile
$otherCreator
;
private
User
$otherCompanyUser
;
private
CompanyProfile
$otherCompany
;
private
User
$admin
;
private
Campaign
$campaign
;
private
Project
$project
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
]);
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
();
$this
->
company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
]);
$this
->
otherCreatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
otherCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCreatorUser
->
id
]);
$this
->
otherCompanyUser
=
User
::
factory
()
->
company
()
->
create
();
$this
->
otherCompany
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
otherCompanyUser
->
id
]);
$this
->
admin
=
User
::
factory
()
->
admin
()
->
create
();
$this
->
campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$this
->
company
->
id
]);
$this
->
project
=
Project
::
factory
()
->
create
([
'campaign_id'
=>
$this
->
campaign
->
id
,
'company_id'
=>
$this
->
company
->
id
,
'creator_id'
=>
$this
->
creator
->
id
,
'status'
=>
'not_started'
,
]);
}
// ─── viewAny ────────────────────────────────────────────────────────
public
function
test_creator_can_view_any
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'viewAny'
,
Project
::
class
));
}
public
function
test_company_can_view_any
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'viewAny'
,
Project
::
class
));
}
public
function
test_admin_can_view_any
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'viewAny'
,
Project
::
class
));
}
// ─── view ───────────────────────────────────────────────────────────
public
function
test_project_creator_can_view
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'view'
,
$this
->
project
));
}
public
function
test_project_company_can_view
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'view'
,
$this
->
project
));
}
public
function
test_admin_can_view_any_project
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'view'
,
$this
->
project
));
}
public
function
test_other_creator_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'view'
,
$this
->
project
));
}
public
function
test_other_company_cannot_view
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'view'
,
$this
->
project
));
}
// ─── start ──────────────────────────────────────────────────────────
public
function
test_creator_can_start_own_not_started_project
()
:
void
{
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'start'
,
$this
->
project
));
}
public
function
test_creator_cannot_start_already_started_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'start'
,
$this
->
project
));
}
public
function
test_company_cannot_start_project
()
:
void
{
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'start'
,
$this
->
project
));
}
public
function
test_other_creator_cannot_start
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'start'
,
$this
->
project
));
}
// ─── cancel ─────────────────────────────────────────────────────────
public
function
test_creator_can_cancel_own_cancellable_project
()
:
void
{
$result
=
$this
->
creatorUser
->
can
(
'cancel'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBeCancelled
(),
$result
);
}
public
function
test_company_can_cancel_own_cancellable_project
()
:
void
{
$result
=
$this
->
companyUser
->
can
(
'cancel'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBeCancelled
(),
$result
);
}
public
function
test_admin_can_cancel_cancellable_project
()
:
void
{
$result
=
$this
->
admin
->
can
(
'cancel'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBeCancelled
(),
$result
);
}
public
function
test_other_creator_cannot_cancel
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'cancel'
,
$this
->
project
));
}
public
function
test_other_company_cannot_cancel
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'cancel'
,
$this
->
project
));
}
public
function
test_nobody_can_cancel_completed_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'completed'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'cancel'
,
$this
->
project
));
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'cancel'
,
$this
->
project
));
$this
->
assertFalse
(
$this
->
admin
->
can
(
'cancel'
,
$this
->
project
));
}
// ─── dispute ────────────────────────────────────────────────────────
public
function
test_creator_can_dispute_disputable_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$result
=
$this
->
creatorUser
->
can
(
'dispute'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBeDisputed
(),
$result
);
}
public
function
test_company_can_dispute_own_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$result
=
$this
->
companyUser
->
can
(
'dispute'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBeDisputed
(),
$result
);
}
public
function
test_admin_cannot_dispute
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
admin
->
can
(
'dispute'
,
$this
->
project
));
}
public
function
test_other_creator_cannot_dispute
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'dispute'
,
$this
->
project
));
}
// ─── requestExtension ───────────────────────────────────────────────
public
function
test_creator_can_request_extension_on_active_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'requestExtension'
,
$this
->
project
));
}
public
function
test_company_cannot_request_extension
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'requestExtension'
,
$this
->
project
));
}
public
function
test_creator_cannot_request_extension_on_completed_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'completed'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'requestExtension'
,
$this
->
project
));
}
// ─── respondToExtension ─────────────────────────────────────────────
public
function
test_company_can_respond_to_extension_when_pending
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
project
->
deadlineExtensions
()
->
create
([
'requested_by'
=>
$this
->
creatorUser
->
id
,
'reason'
=>
'Need more time.'
,
'requested_days'
=>
7
,
'status'
=>
'pending'
,
]);
$this
->
project
->
refresh
();
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'respondToExtension'
,
$this
->
project
));
}
public
function
test_creator_cannot_respond_to_extension
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
project
->
deadlineExtensions
()
->
create
([
'requested_by'
=>
$this
->
creatorUser
->
id
,
'reason'
=>
'Need more time.'
,
'requested_days'
=>
7
,
'status'
=>
'pending'
,
]);
$this
->
project
->
refresh
();
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'respondToExtension'
,
$this
->
project
));
}
public
function
test_company_cannot_respond_when_no_pending_extension
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
companyUser
->
can
(
'respondToExtension'
,
$this
->
project
));
}
// ─── putOnHold ──────────────────────────────────────────────────────
public
function
test_creator_can_put_holdable_project_on_hold
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$result
=
$this
->
creatorUser
->
can
(
'putOnHold'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBePutOnHold
(),
$result
);
}
public
function
test_company_can_put_holdable_project_on_hold
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$result
=
$this
->
companyUser
->
can
(
'putOnHold'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBePutOnHold
(),
$result
);
}
public
function
test_admin_can_put_holdable_project_on_hold
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$result
=
$this
->
admin
->
can
(
'putOnHold'
,
$this
->
project
);
$this
->
assertEquals
(
$this
->
project
->
canBePutOnHold
(),
$result
);
}
public
function
test_outsider_cannot_put_on_hold
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'putOnHold'
,
$this
->
project
));
}
// ─── resume ─────────────────────────────────────────────────────────
public
function
test_creator_can_resume_on_hold_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'on_hold'
]);
$this
->
assertTrue
(
$this
->
creatorUser
->
can
(
'resume'
,
$this
->
project
));
}
public
function
test_company_can_resume_on_hold_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'on_hold'
]);
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'resume'
,
$this
->
project
));
}
public
function
test_admin_can_resume_on_hold_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'on_hold'
]);
$this
->
assertTrue
(
$this
->
admin
->
can
(
'resume'
,
$this
->
project
));
}
public
function
test_cannot_resume_non_hold_project
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'in_progress'
]);
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'resume'
,
$this
->
project
));
}
public
function
test_outsider_cannot_resume
()
:
void
{
$this
->
project
->
update
([
'status'
=>
'on_hold'
]);
$this
->
assertFalse
(
$this
->
otherCreatorUser
->
can
(
'resume'
,
$this
->
project
));
}
// ─── manage ─────────────────────────────────────────────────────────
public
function
test_company_can_manage_own_project
()
:
void
{
$this
->
assertTrue
(
$this
->
companyUser
->
can
(
'manage'
,
$this
->
project
));
}
public
function
test_admin_can_manage_any_project
()
:
void
{
$this
->
assertTrue
(
$this
->
admin
->
can
(
'manage'
,
$this
->
project
));
}
public
function
test_creator_cannot_manage
()
:
void
{
$this
->
assertFalse
(
$this
->
creatorUser
->
can
(
'manage'
,
$this
->
project
));
}
public
function
test_other_company_cannot_manage
()
:
void
{
$this
->
assertFalse
(
$this
->
otherCompanyUser
->
can
(
'manage'
,
$this
->
project
));
}
}
tests/Feature/Reporting/ReportServiceTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Reporting
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Reporting\Models\Report
;
use
App\Modules\Reporting\Services\ReportService
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Illuminate\Support\Facades\DB
;
use
Tests\TestCase
;
class
ReportServiceTest
extends
TestCase
{
use
RefreshDatabase
;
private
ReportService
$service
;
private
User
$reporter
;
private
User
$admin
;
private
User
$targetUser
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
service
=
app
(
ReportService
::
class
);
$this
->
reporter
=
User
::
factory
()
->
creator
()
->
create
();
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
reporter
->
id
]);
$this
->
admin
=
User
::
factory
()
->
admin
()
->
create
();
$this
->
targetUser
=
User
::
factory
()
->
creator
()
->
create
();
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
targetUser
->
id
]);
}
// ─── Report Submission ──────────────────────────────────────────────
public
function
test_submit_creates_report_with_required_fields
()
:
void
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
,
'description'
=>
null
],
);
$this
->
assertInstanceOf
(
Report
::
class
,
$report
);
$this
->
assertEquals
(
$this
->
reporter
->
id
,
$report
->
reporter_id
);
$this
->
assertEquals
(
'user'
,
$report
->
target_type
);
$this
->
assertEquals
(
$this
->
targetUser
->
id
,
$report
->
target_id
);
$this
->
assertEquals
(
'spam'
,
$report
->
reason
);
$this
->
assertNull
(
$report
->
description
);
$this
->
assertEquals
(
'pending'
,
$report
->
status
);
$this
->
assertNotNull
(
$report
->
uuid
);
}
public
function
test_submit_creates_report_with_description
()
:
void
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'other'
,
'description'
=>
'This user is sending unsolicited messages to multiple creators.'
],
);
$this
->
assertEquals
(
'other'
,
$report
->
reason
);
$this
->
assertEquals
(
'This user is sending unsolicited messages to multiple creators.'
,
$report
->
description
);
}
public
function
test_submit_uses_pending_status_by_default
()
:
void
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'harassment'
],
);
$this
->
assertEquals
(
'pending'
,
$report
->
status
);
$this
->
assertNull
(
$report
->
resolved_by
);
$this
->
assertNull
(
$report
->
resolved_at
);
$this
->
assertNull
(
$report
->
resolution_notes
);
}
public
function
test_submit_generates_uuid_for_report
()
:
void
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
'campaign'
,
999
,
[
'reason'
=>
'fraud'
],
);
$this
->
assertNotNull
(
$report
->
uuid
);
$this
->
assertMatchesRegularExpression
(
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/'
,
$report
->
uuid
);
}
public
function
test_submit_supports_all_valid_target_types
()
:
void
{
$types
=
[
'user'
,
'campaign'
,
'message'
,
'review'
,
'portfolio_item'
];
foreach
(
$types
as
$type
)
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
$type
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$this
->
assertEquals
(
$type
,
$report
->
target_type
);
}
$this
->
assertDatabaseCount
(
'reports'
,
count
(
$types
));
}
// ─── Duplicate Prevention ───────────────────────────────────────────
public
function
test_submit_rejects_duplicate_pending_report_for_same_target
()
:
void
{
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$this
->
expectException
(
\Symfony\Component\HttpKernel\Exception\HttpException
::
class
);
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'harassment'
],
);
}
public
function
test_submit_rejects_duplicate_reviewing_report_for_same_target
()
:
void
{
$report
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$report
->
update
([
'status'
=>
'reviewing'
]);
$this
->
expectException
(
\Symfony\Component\HttpKernel\Exception\HttpException
::
class
);
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'harassment'
],
);
}
public
function
test_submit_allows_report_after_previous_was_resolved
()
:
void
{
$first
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$first
->
update
([
'status'
=>
'invalid'
]);
$second
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'harassment'
],
);
$this
->
assertNotEquals
(
$first
->
id
,
$second
->
id
);
$this
->
assertEquals
(
'pending'
,
$second
->
status
);
}
public
function
test_submit_allows_same_target_from_different_reporters
()
:
void
{
$otherReporter
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$second
=
$this
->
service
->
submit
(
$otherReporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'harassment'
],
);
$this
->
assertNotNull
(
$second
->
id
);
$this
->
assertDatabaseCount
(
'reports'
,
2
);
}
public
function
test_submit_allows_same_reporter_different_targets
()
:
void
{
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$second
=
$this
->
service
->
submit
(
$this
->
reporter
,
'campaign'
,
999
,
[
'reason'
=>
'fraud'
],
);
$this
->
assertNotNull
(
$second
->
id
);
$this
->
assertDatabaseCount
(
'reports'
,
2
);
}
// ─── Rate Limiting ──────────────────────────────────────────────────
public
function
test_submit_enforces_daily_rate_limit
()
:
void
{
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$i
+
100
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
}
$this
->
expectException
(
\Symfony\Component\HttpKernel\Exception\HttpException
::
class
);
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
999
,
[
'reason'
=>
'spam'
],
);
}
public
function
test_submit_rate_limit_resets_next_day
()
:
void
{
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$i
+
100
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$report
->
update
([
'created_at'
=>
now
()
->
subDay
()]);
}
$newReport
=
$this
->
service
->
submit
(
$this
->
reporter
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$this
->
assertNotNull
(
$newReport
->
id
);
}
public
function
test_submit_rate_limit_is_per_user
()
:
void
{
for
(
$i
=
0
;
$i
<
10
;
$i
++
)
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$i
+
100
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
}
$otherUser
=
User
::
factory
()
->
creator
()
->
create
();
$report
=
$this
->
service
->
submit
(
$otherUser
,
'user'
,
$this
->
targetUser
->
id
,
[
'reason'
=>
'spam'
],
);
$this
->
assertNotNull
(
$report
->
id
);
}
// ─── Report Resolution ──────────────────────────────────────────────
public
function
test_resolve_updates_status_to_valid
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
service
->
resolve
(
$report
,
$this
->
admin
,
'valid'
,
'User confirmed as spammer.'
);
$report
->
refresh
();
$this
->
assertEquals
(
'valid'
,
$report
->
status
);
$this
->
assertEquals
(
$this
->
admin
->
id
,
$report
->
resolved_by
);
$this
->
assertEquals
(
'User confirmed as spammer.'
,
$report
->
resolution_notes
);
$this
->
assertNotNull
(
$report
->
resolved_at
);
}
public
function
test_resolve_updates_status_to_invalid
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$this
->
service
->
resolve
(
$report
,
$this
->
admin
,
'invalid'
,
'No evidence of spam.'
);
$report
->
refresh
();
$this
->
assertEquals
(
'invalid'
,
$report
->
status
);
$this
->
assertEquals
(
'No evidence of spam.'
,
$report
->
resolution_notes
);
}
public
function
test_resolve_sets_resolved_at_timestamp
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'harassment'
,
'status'
=>
'pending'
,
]);
$before
=
now
();
$this
->
service
->
resolve
(
$report
,
$this
->
admin
,
'valid'
);
$report
->
refresh
();
$this
->
assertGreaterThanOrEqual
(
$before
->
timestamp
,
$report
->
resolved_at
->
timestamp
);
}
public
function
test_resolve_allows_null_notes
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
service
->
resolve
(
$report
,
$this
->
admin
,
'valid'
);
$report
->
refresh
();
$this
->
assertNull
(
$report
->
resolution_notes
);
}
public
function
test_resolve_creates_admin_activity_log
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'fraud'
,
'status'
=>
'reviewing'
,
]);
$this
->
actingAs
(
$this
->
admin
);
$this
->
service
->
resolve
(
$report
,
$this
->
admin
,
'valid'
,
'Confirmed fraud.'
);
$this
->
assertDatabaseHas
(
'admin_activity_logs'
,
[
'admin_id'
=>
$this
->
admin
->
id
,
'action'
=>
'report_resolved'
,
'target_type'
=>
'report'
,
'target_id'
=>
$report
->
id
,
]);
}
// ─── Admin Queue ────────────────────────────────────────────────────
public
function
test_get_admin_queue_returns_pending_and_reviewing_by_default
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'campaign'
,
'target_id'
=>
2
,
'reason'
=>
'fraud'
,
'status'
=>
'reviewing'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
3
,
'reason'
=>
'spam'
,
'status'
=>
'valid'
,
]);
$result
=
$this
->
service
->
getAdminQueue
();
$this
->
assertEquals
(
2
,
$result
->
total
());
}
public
function
test_get_admin_queue_filters_by_status
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$result
=
$this
->
service
->
getAdminQueue
([
'status'
=>
'pending'
]);
$this
->
assertEquals
(
1
,
$result
->
total
());
$this
->
assertEquals
(
'pending'
,
$result
->
first
()
->
status
);
}
public
function
test_get_admin_queue_filters_by_target_type
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'campaign'
,
'target_id'
=>
2
,
'reason'
=>
'fraud'
,
'status'
=>
'pending'
,
]);
$result
=
$this
->
service
->
getAdminQueue
([
'target_type'
=>
'campaign'
]);
$this
->
assertEquals
(
1
,
$result
->
total
());
$this
->
assertEquals
(
'campaign'
,
$result
->
first
()
->
target_type
);
}
public
function
test_get_admin_queue_orders_by_oldest_first
()
:
void
{
$old
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
'created_at'
=>
now
()
->
subDays
(
5
),
]);
$new
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
'created_at'
=>
now
()
->
subDay
(),
]);
$result
=
$this
->
service
->
getAdminQueue
();
$this
->
assertEquals
(
$old
->
id
,
$result
->
first
()
->
id
);
}
public
function
test_get_admin_queue_eager_loads_reporter
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$result
=
$this
->
service
->
getAdminQueue
();
$this
->
assertTrue
(
$result
->
first
()
->
relationLoaded
(
'reporter'
));
}
public
function
test_get_admin_queue_paginates_results
()
:
void
{
for
(
$i
=
0
;
$i
<
30
;
$i
++
)
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$i
+
100
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
}
$result
=
$this
->
service
->
getAdminQueue
();
$this
->
assertEquals
(
25
,
$result
->
perPage
());
$this
->
assertEquals
(
30
,
$result
->
total
());
}
// ─── User Reports ───────────────────────────────────────────────────
public
function
test_get_user_reports_returns_only_own_reports
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$otherUser
=
User
::
factory
()
->
creator
()
->
create
();
Report
::
create
([
'reporter_id'
=>
$otherUser
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$result
=
$this
->
service
->
getUserReports
(
$this
->
reporter
);
$this
->
assertEquals
(
1
,
$result
->
total
());
$this
->
assertEquals
(
$this
->
reporter
->
id
,
$result
->
first
()
->
reporter_id
);
}
public
function
test_get_user_reports_orders_by_newest_first
()
:
void
{
$old
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
'created_at'
=>
now
()
->
subDays
(
5
),
]);
$new
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'fraud'
,
'status'
=>
'pending'
,
'created_at'
=>
now
(),
]);
$result
=
$this
->
service
->
getUserReports
(
$this
->
reporter
);
$this
->
assertEquals
(
$new
->
id
,
$result
->
first
()
->
id
);
}
public
function
test_get_user_reports_includes_all_statuses
()
:
void
{
$statuses
=
[
'pending'
,
'reviewing'
,
'valid'
,
'invalid'
];
foreach
(
$statuses
as
$i
=>
$status
)
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$i
+
100
,
'reason'
=>
'spam'
,
'status'
=>
$status
,
]);
}
$result
=
$this
->
service
->
getUserReports
(
$this
->
reporter
);
$this
->
assertEquals
(
4
,
$result
->
total
());
}
// ─── Model Scopes ───────────────────────────────────────────────────
public
function
test_scope_pending_returns_only_pending
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$result
=
Report
::
pending
()
->
get
();
$this
->
assertCount
(
1
,
$result
);
$this
->
assertEquals
(
'pending'
,
$result
->
first
()
->
status
);
}
public
function
test_scope_reviewing_returns_only_reviewing
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$result
=
Report
::
reviewing
()
->
get
();
$this
->
assertCount
(
1
,
$result
);
$this
->
assertEquals
(
'reviewing'
,
$result
->
first
()
->
status
);
}
public
function
test_scope_resolved_returns_valid_and_invalid
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'valid'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
3
,
'reason'
=>
'spam'
,
'status'
=>
'invalid'
,
]);
$result
=
Report
::
resolved
()
->
get
();
$this
->
assertCount
(
2
,
$result
);
}
public
function
test_scope_for_reporter_filters_by_user
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$otherUser
=
User
::
factory
()
->
creator
()
->
create
();
Report
::
create
([
'reporter_id'
=>
$otherUser
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
2
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$result
=
Report
::
forReporter
(
$this
->
reporter
->
id
)
->
get
();
$this
->
assertCount
(
1
,
$result
);
$this
->
assertEquals
(
$this
->
reporter
->
id
,
$result
->
first
()
->
reporter_id
);
}
public
function
test_scope_for_target_filters_by_type_and_id
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
42
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'campaign'
,
'target_id'
=>
42
,
'reason'
=>
'fraud'
,
'status'
=>
'pending'
,
]);
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
99
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$result
=
Report
::
forTarget
(
'user'
,
42
)
->
get
();
$this
->
assertCount
(
1
,
$result
);
$this
->
assertEquals
(
'user'
,
$result
->
first
()
->
target_type
);
$this
->
assertEquals
(
42
,
$result
->
first
()
->
target_id
);
}
// ─── Model Relationships ────────────────────────────────────────────
public
function
test_report_belongs_to_reporter
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
assertEquals
(
$this
->
reporter
->
id
,
$report
->
reporter
->
id
);
$this
->
assertEquals
(
$this
->
reporter
->
email
,
$report
->
reporter
->
email
);
}
public
function
test_report_belongs_to_resolver_when_resolved
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'valid'
,
'resolved_by'
=>
$this
->
admin
->
id
,
'resolved_at'
=>
now
(),
]);
$this
->
assertEquals
(
$this
->
admin
->
id
,
$report
->
resolver
->
id
);
}
public
function
test_report_resolver_is_null_when_unresolved
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
assertNull
(
$report
->
resolver
);
}
// ─── Route Key Name ─────────────────────────────────────────────────
public
function
test_report_uses_uuid_as_route_key
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
reporter
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
assertEquals
(
'uuid'
,
$report
->
getRouteKeyName
());
}
}
tests/Feature/Reporting/ReportingApiTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Reporting
;
use
App\Models\User
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Reporting\Models\Report
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Illuminate\Http\UploadedFile
;
use
Illuminate\Support\Facades\DB
;
use
Illuminate\Support\Facades\Storage
;
use
Illuminate\Support\Str
;
use
Tests\TestCase
;
class
ReportingApiTest
extends
TestCase
{
use
RefreshDatabase
;
private
User
$creator
;
private
User
$company
;
private
User
$admin
;
private
User
$targetUser
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
creator
=
User
::
factory
()
->
creator
()
->
create
([
'email_verified_at'
=>
now
()]);
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creator
->
id
]);
$this
->
company
=
User
::
factory
()
->
company
()
->
create
([
'email_verified_at'
=>
now
()]);
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
company
->
id
]);
$this
->
admin
=
User
::
factory
()
->
admin
()
->
create
([
'email_verified_at'
=>
now
()]);
$this
->
targetUser
=
User
::
factory
()
->
creator
()
->
create
();
}
// ─── Submit Report (POST /reports) ──────────────────────────────────
public
function
test_authenticated_user_can_submit_report
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
uuid
,
'reason'
=>
'spam'
,
]);
$response
->
assertStatus
(
201
);
$response
->
assertJson
([
'success'
=>
true
]);
}
public
function
test_unauthenticated_user_cannot_submit_report
()
:
void
{
$response
=
$this
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'spam'
,
]);
$response
->
assertStatus
(
401
);
}
public
function
test_submit_report_validates_target_type
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'invalid_type'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'spam'
,
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'target_type'
]);
}
public
function
test_submit_report_validates_reason
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'invalid_reason'
,
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'reason'
]);
}
public
function
test_submit_report_requires_target_id_as_uuid
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
'12345'
,
'reason'
=>
'spam'
,
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'target_id'
]);
}
public
function
test_submit_report_requires_description_when_reason_is_other
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'other'
,
'description'
=>
null
,
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'description'
]);
}
public
function
test_submit_report_description_must_be_at_least_20_chars
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'other'
,
'description'
=>
'Too short.'
,
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'description'
]);
}
public
function
test_submit_report_description_max_2000_chars
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'other'
,
'description'
=>
str_repeat
(
'a'
,
2001
),
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'description'
]);
}
public
function
test_submit_report_accepts_valid_description
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
uuid
,
'reason'
=>
'other'
,
'description'
=>
'This user keeps sending me spam messages repeatedly and will not stop despite being asked.'
,
]);
$response
->
assertStatus
(
201
);
}
public
function
test_submit_report_validates_evidence_max_3_files
()
:
void
{
Storage
::
fake
(
'local'
);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'harassment'
,
'evidence'
=>
[
UploadedFile
::
fake
()
->
image
(
'a.jpg'
,
100
,
100
),
UploadedFile
::
fake
()
->
image
(
'b.jpg'
,
100
,
100
),
UploadedFile
::
fake
()
->
image
(
'c.jpg'
,
100
,
100
),
UploadedFile
::
fake
()
->
image
(
'd.jpg'
,
100
,
100
),
],
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'evidence'
]);
}
public
function
test_submit_report_validates_evidence_mime_types
()
:
void
{
Storage
::
fake
(
'local'
);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'harassment'
,
'evidence'
=>
[
UploadedFile
::
fake
()
->
create
(
'script.js'
,
100
,
'application/javascript'
),
],
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'evidence.0'
]);
}
public
function
test_submit_report_validates_evidence_max_size_5mb
()
:
void
{
Storage
::
fake
(
'local'
);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'harassment'
,
'evidence'
=>
[
UploadedFile
::
fake
()
->
create
(
'big.png'
,
6000
,
'image/png'
),
],
]);
$response
->
assertStatus
(
422
);
$response
->
assertJsonValidationErrors
([
'evidence.0'
]);
}
public
function
test_submit_report_all_valid_reasons_accepted
()
:
void
{
$reasons
=
[
'spam'
,
'harassment'
,
'inappropriate_content'
,
'fraud'
,
'impersonation'
,
'copyright'
,
'other'
];
foreach
(
$reasons
as
$i
=>
$reason
)
{
$data
=
[
'target_type'
=>
'user'
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
$reason
,
];
if
(
$reason
===
'other'
)
{
$data
[
'description'
]
=
'This is a detailed description of the issue that is at least twenty characters.'
;
}
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
$data
);
$this
->
assertContains
(
$response
->
status
(),
[
201
,
422
],
"Failed for reason:
{
$reason
}
"
);
}
}
public
function
test_submit_report_all_valid_target_types_accepted
()
:
void
{
$types
=
[
'user'
,
'campaign'
,
'message'
,
'review'
,
'portfolio_item'
];
foreach
(
$types
as
$type
)
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
$type
,
'target_id'
=>
Str
::
uuid
()
->
toString
(),
'reason'
=>
'spam'
,
]);
$this
->
assertContains
(
$response
->
status
(),
[
201
,
422
],
"Failed for target_type:
{
$type
}
"
);
}
}
public
function
test_company_user_can_submit_report
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
company
)
->
postJson
(
'/reports'
,
[
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
uuid
,
'reason'
=>
'fraud'
,
]);
$response
->
assertStatus
(
201
);
}
// ─── My Reports (GET /my-reports) ───────────────────────────────────
public
function
test_user_can_view_own_reports
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
get
(
'/my-reports'
);
$response
->
assertStatus
(
200
);
}
public
function
test_unauthenticated_user_cannot_view_reports
()
:
void
{
$response
=
$this
->
get
(
'/my-reports'
);
$response
->
assertRedirect
();
}
// ─── Admin: Report Queue (GET /admin/reports) ───────────────────────
public
function
test_admin_can_view_report_queue
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
get
(
'/admin/reports'
);
$response
->
assertStatus
(
200
);
}
public
function
test_non_admin_cannot_view_report_queue
()
:
void
{
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
get
(
'/admin/reports'
);
$response
->
assertStatus
(
403
);
}
public
function
test_admin_queue_filters_by_status
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
1
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
get
(
'/admin/reports?status=pending'
);
$response
->
assertStatus
(
200
);
}
public
function
test_admin_queue_filters_by_target_type
()
:
void
{
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'campaign'
,
'target_id'
=>
1
,
'reason'
=>
'fraud'
,
'status'
=>
'pending'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
get
(
'/admin/reports?target_type=campaign'
);
$response
->
assertStatus
(
200
);
}
// ─── Admin: Show Report (GET /admin/reports/{report}) ───────────────
public
function
test_admin_can_view_individual_report
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
get
(
"/admin/reports/
{
$report
->
uuid
}
"
);
$response
->
assertStatus
(
200
);
}
public
function
test_viewing_pending_report_transitions_to_reviewing
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$this
->
actingAs
(
$this
->
admin
)
->
get
(
"/admin/reports/
{
$report
->
uuid
}
"
);
$report
->
refresh
();
$this
->
assertEquals
(
'reviewing'
,
$report
->
status
);
}
public
function
test_viewing_reviewing_report_does_not_change_status
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$this
->
actingAs
(
$this
->
admin
)
->
get
(
"/admin/reports/
{
$report
->
uuid
}
"
);
$report
->
refresh
();
$this
->
assertEquals
(
'reviewing'
,
$report
->
status
);
}
public
function
test_non_admin_cannot_view_individual_report
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'pending'
,
]);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
get
(
"/admin/reports/
{
$report
->
uuid
}
"
);
$response
->
assertStatus
(
403
);
}
// ─── Admin: Resolve Report (POST /admin/reports/{report}/resolve) ───
public
function
test_admin_can_resolve_report_as_valid
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'valid'
,
'notes'
=>
'Confirmed spam behavior.'
,
]);
$response
->
assertRedirect
();
$response
->
assertSessionHas
(
'success'
);
$report
->
refresh
();
$this
->
assertEquals
(
'valid'
,
$report
->
status
);
$this
->
assertEquals
(
$this
->
admin
->
id
,
$report
->
resolved_by
);
}
public
function
test_admin_can_resolve_report_as_invalid
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'harassment'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'invalid'
,
'notes'
=>
'No evidence of harassment found.'
,
]);
$response
->
assertRedirect
();
$report
->
refresh
();
$this
->
assertEquals
(
'invalid'
,
$report
->
status
);
}
public
function
test_resolve_requires_valid_status
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'pending'
,
]);
$response
->
assertSessionHasErrors
([
'status'
]);
}
public
function
test_non_admin_cannot_resolve_report
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
creator
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'valid'
,
]);
$response
->
assertStatus
(
403
);
}
public
function
test_resolve_notes_are_optional
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'valid'
,
]);
$response
->
assertRedirect
();
$report
->
refresh
();
$this
->
assertEquals
(
'valid'
,
$report
->
status
);
$this
->
assertNull
(
$report
->
resolution_notes
);
}
public
function
test_resolve_notes_max_2000_chars
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'spam'
,
'status'
=>
'reviewing'
,
]);
$response
=
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'valid'
,
'notes'
=>
str_repeat
(
'a'
,
2001
),
]);
$response
->
assertSessionHasErrors
([
'notes'
]);
}
public
function
test_resolve_creates_audit_log_entry
()
:
void
{
$report
=
Report
::
create
([
'reporter_id'
=>
$this
->
creator
->
id
,
'target_type'
=>
'user'
,
'target_id'
=>
$this
->
targetUser
->
id
,
'reason'
=>
'fraud'
,
'status'
=>
'reviewing'
,
]);
$this
->
actingAs
(
$this
->
admin
)
->
post
(
"/admin/reports/
{
$report
->
uuid
}
/resolve"
,
[
'status'
=>
'valid'
,
'notes'
=>
'Fraud confirmed.'
,
]);
$this
->
assertDatabaseHas
(
'admin_activity_logs'
,
[
'admin_id'
=>
$this
->
admin
->
id
,
'action'
=>
'report_resolved'
,
'target_type'
=>
'report'
,
'target_id'
=>
$report
->
id
,
]);
}
}
tests/Feature/Reputation/ReputationServiceTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\Reputation
;
use
App\Models\User
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Projects\Models\Project
;
use
App\Modules\Reputation\Enums\ReputationTier
;
use
App\Modules\Reputation\Services\ReputationService
;
use
App\Modules\Reviews\Models\Review
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Illuminate\Support\Facades\DB
;
use
Tests\TestCase
;
class
ReputationServiceTest
extends
TestCase
{
use
RefreshDatabase
;
private
ReputationService
$service
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
User
$companyUser
;
private
CompanyProfile
$company
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
service
=
app
(
ReputationService
::
class
);
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'active'
,
'created_at'
=>
now
()
->
subMonths
(
6
),
]);
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
,
'last_active_at'
=>
now
(),
'completion_percentage'
=>
80
,
'avg_response_hours'
=>
4
,
]);
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
([
'status'
=>
'active'
,
'created_at'
=>
now
()
->
subMonths
(
12
),
]);
$this
->
company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
,
'completion_percentage'
=>
90
,
]);
}
// ─── ReputationTier Enum ────────────────────────────────────────────
public
function
test_tier_from_score_new
()
:
void
{
$this
->
assertEquals
(
ReputationTier
::
New
,
ReputationTier
::
fromScore
(
0
));
$this
->
assertEquals
(
ReputationTier
::
New
,
ReputationTier
::
fromScore
(
10
));
$this
->
assertEquals
(
ReputationTier
::
New
,
ReputationTier
::
fromScore
(
24
));
}
public
function
test_tier_from_score_rising
()
:
void
{
$this
->
assertEquals
(
ReputationTier
::
Rising
,
ReputationTier
::
fromScore
(
25
));
$this
->
assertEquals
(
ReputationTier
::
Rising
,
ReputationTier
::
fromScore
(
35
));
$this
->
assertEquals
(
ReputationTier
::
Rising
,
ReputationTier
::
fromScore
(
49
));
}
public
function
test_tier_from_score_established
()
:
void
{
$this
->
assertEquals
(
ReputationTier
::
Established
,
ReputationTier
::
fromScore
(
50
));
$this
->
assertEquals
(
ReputationTier
::
Established
,
ReputationTier
::
fromScore
(
60
));
$this
->
assertEquals
(
ReputationTier
::
Established
,
ReputationTier
::
fromScore
(
69
));
}
public
function
test_tier_from_score_top_rated
()
:
void
{
$this
->
assertEquals
(
ReputationTier
::
TopRated
,
ReputationTier
::
fromScore
(
70
));
$this
->
assertEquals
(
ReputationTier
::
TopRated
,
ReputationTier
::
fromScore
(
80
));
$this
->
assertEquals
(
ReputationTier
::
TopRated
,
ReputationTier
::
fromScore
(
84
));
}
public
function
test_tier_from_score_elite
()
:
void
{
$this
->
assertEquals
(
ReputationTier
::
Elite
,
ReputationTier
::
fromScore
(
85
));
$this
->
assertEquals
(
ReputationTier
::
Elite
,
ReputationTier
::
fromScore
(
95
));
$this
->
assertEquals
(
ReputationTier
::
Elite
,
ReputationTier
::
fromScore
(
100
));
}
public
function
test_tier_icons_defined
()
:
void
{
foreach
(
ReputationTier
::
cases
()
as
$tier
)
{
$this
->
assertNotEmpty
(
$tier
->
icon
());
}
}
public
function
test_tier_colors_defined
()
:
void
{
foreach
(
ReputationTier
::
cases
()
as
$tier
)
{
$this
->
assertNotEmpty
(
$tier
->
color
());
}
}
public
function
test_tier_min_scores_are_ordered
()
:
void
{
$scores
=
array_map
(
fn
(
$t
)
=>
$t
->
minScore
(),
ReputationTier
::
cases
());
$sorted
=
$scores
;
sort
(
$sorted
);
$this
->
assertEquals
(
$sorted
,
$scores
);
}
// ─── Creator Score Calculation ──────────────────────────────────────
public
function
test_calculate_creator_score_returns_integer
()
:
void
{
$score
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertIsInt
(
$score
);
}
public
function
test_calculate_creator_score_is_between_0_and_100
()
:
void
{
$score
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertGreaterThanOrEqual
(
0
,
$score
);
$this
->
assertLessThanOrEqual
(
100
,
$score
);
}
public
function
test_calculate_creator_score_updates_profile
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
creator
->
refresh
();
$this
->
assertNotNull
(
$this
->
creator
->
reputation_score
);
$this
->
assertNotNull
(
$this
->
creator
->
reputation_tier
);
}
public
function
test_calculate_creator_score_with_no_data_gives_moderate_score
()
:
void
{
$freshCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
User
::
factory
()
->
creator
()
->
create
([
'created_at'
=>
now
()])
->
id
,
'last_active_at'
=>
now
(),
'completion_percentage'
=>
0
,
'avg_response_hours'
=>
null
,
]);
$score
=
$this
->
service
->
calculateCreatorScore
(
$freshCreator
);
$this
->
assertGreaterThanOrEqual
(
15
,
$score
);
$this
->
assertLessThanOrEqual
(
60
,
$score
);
}
public
function
test_calculate_creator_score_rewards_good_reviews
()
:
void
{
$reviewerUser
=
User
::
factory
()
->
company
()
->
create
();
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
Review
::
create
([
'reviewer_id'
=>
$reviewerUser
->
id
,
'reviewee_id'
=>
$this
->
creatorUser
->
id
,
'project_id'
=>
null
,
'overall_rating'
=>
5
,
'communication_rating'
=>
5
,
'quality_rating'
=>
5
,
'timeliness_rating'
=>
5
,
'comment'
=>
'Excellent work.'
,
'status'
=>
'published'
,
]);
}
$score
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertGreaterThanOrEqual
(
40
,
$score
);
}
public
function
test_calculate_creator_score_penalizes_bad_reviews
()
:
void
{
$reviewerUser
=
User
::
factory
()
->
company
()
->
create
();
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
Review
::
create
([
'reviewer_id'
=>
$reviewerUser
->
id
,
'reviewee_id'
=>
$this
->
creatorUser
->
id
,
'project_id'
=>
null
,
'overall_rating'
=>
1
,
'communication_rating'
=>
1
,
'quality_rating'
=>
1
,
'timeliness_rating'
=>
1
,
'comment'
=>
'Poor work.'
,
'status'
=>
'published'
,
]);
}
$scoreWithBadReviews
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$goodCreatorUser
=
User
::
factory
()
->
creator
()
->
create
([
'created_at'
=>
now
()
->
subMonths
(
6
)]);
$goodCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$goodCreatorUser
->
id
,
'last_active_at'
=>
now
(),
'completion_percentage'
=>
80
,
'avg_response_hours'
=>
4
,
]);
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
Review
::
create
([
'reviewer_id'
=>
$reviewerUser
->
id
,
'reviewee_id'
=>
$goodCreatorUser
->
id
,
'project_id'
=>
null
,
'overall_rating'
=>
5
,
'communication_rating'
=>
5
,
'quality_rating'
=>
5
,
'timeliness_rating'
=>
5
,
'comment'
=>
'Great work.'
,
'status'
=>
'published'
,
]);
}
$scoreWithGoodReviews
=
$this
->
service
->
calculateCreatorScore
(
$goodCreator
);
$this
->
assertLessThan
(
$scoreWithGoodReviews
,
$scoreWithBadReviews
);
}
// ─── Decay ──────────────────────────────────────────────────────────
public
function
test_no_decay_within_90_days
()
:
void
{
$this
->
creator
->
update
([
'last_active_at'
=>
now
()
->
subDays
(
60
)]);
$scoreActive
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
creator
->
update
([
'last_active_at'
=>
now
()]);
$scoreRecent
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertEquals
(
$scoreActive
,
$scoreRecent
);
}
public
function
test_decay_applies_after_90_days
()
:
void
{
$this
->
creator
->
update
([
'last_active_at'
=>
now
()]);
$scoreActive
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
creator
->
update
([
'last_active_at'
=>
now
()
->
subDays
(
180
)]);
$scoreInactive
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertLessThan
(
$scoreActive
,
$scoreInactive
);
}
public
function
test_decay_has_minimum_floor
()
:
void
{
$this
->
creator
->
update
([
'last_active_at'
=>
now
()
->
subYears
(
5
)]);
$score
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertGreaterThanOrEqual
(
10
,
$score
);
}
public
function
test_no_decay_when_last_active_is_null
()
:
void
{
$this
->
creator
->
update
([
'last_active_at'
=>
null
]);
$score
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertGreaterThan
(
10
,
$score
);
}
// ─── Response Time Scoring ──────────────────────────────────────────
public
function
test_fast_response_time_gives_high_score
()
:
void
{
$this
->
creator
->
update
([
'avg_response_hours'
=>
1
]);
$fastScore
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
creator
->
update
([
'avg_response_hours'
=>
72
]);
$slowScore
=
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$this
->
assertGreaterThan
(
$slowScore
,
$fastScore
);
}
// ─── Company Score Calculation ──────────────────────────────────────
public
function
test_calculate_company_score_returns_integer
()
:
void
{
$score
=
$this
->
service
->
calculateCompanyScore
(
$this
->
company
);
$this
->
assertIsInt
(
$score
);
}
public
function
test_calculate_company_score_is_between_0_and_100
()
:
void
{
$score
=
$this
->
service
->
calculateCompanyScore
(
$this
->
company
);
$this
->
assertGreaterThanOrEqual
(
0
,
$score
);
$this
->
assertLessThanOrEqual
(
100
,
$score
);
}
public
function
test_calculate_company_score_updates_profile
()
:
void
{
$this
->
service
->
calculateCompanyScore
(
$this
->
company
);
$this
->
company
->
refresh
();
$this
->
assertNotNull
(
$this
->
company
->
reputation_score
);
}
public
function
test_company_score_penalizes_cancellations
()
:
void
{
$score1
=
$this
->
service
->
calculateCompanyScore
(
$this
->
company
);
$this
->
assertGreaterThanOrEqual
(
0
,
$score1
);
}
// ─── Score Breakdown ────────────────────────────────────────────────
public
function
test_get_score_breakdown_returns_all_factors
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$this
->
assertArrayHasKey
(
'total'
,
$breakdown
);
$this
->
assertArrayHasKey
(
'tier'
,
$breakdown
);
$this
->
assertArrayHasKey
(
'factors'
,
$breakdown
);
$this
->
assertCount
(
8
,
$breakdown
[
'factors'
]);
}
public
function
test_get_score_breakdown_factor_keys
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$keys
=
array_column
(
$breakdown
[
'factors'
],
'key'
);
$this
->
assertContains
(
'reviews'
,
$keys
);
$this
->
assertContains
(
'completion'
,
$keys
);
$this
->
assertContains
(
'on_time'
,
$keys
);
$this
->
assertContains
(
'response_rate'
,
$keys
);
$this
->
assertContains
(
'response_time'
,
$keys
);
$this
->
assertContains
(
'profile'
,
$keys
);
$this
->
assertContains
(
'account_age'
,
$keys
);
$this
->
assertContains
(
'revision_rate'
,
$keys
);
}
public
function
test_get_score_breakdown_weights_sum_to_100
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$totalWeight
=
array_sum
(
array_column
(
$breakdown
[
'factors'
],
'weight'
));
$this
->
assertEquals
(
100
,
$totalWeight
);
}
public
function
test_get_score_breakdown_each_factor_has_required_fields
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
foreach
(
$breakdown
[
'factors'
]
as
$factor
)
{
$this
->
assertArrayHasKey
(
'key'
,
$factor
);
$this
->
assertArrayHasKey
(
'label'
,
$factor
);
$this
->
assertArrayHasKey
(
'score'
,
$factor
);
$this
->
assertArrayHasKey
(
'weight'
,
$factor
);
$this
->
assertArrayHasKey
(
'weighted'
,
$factor
);
$this
->
assertGreaterThanOrEqual
(
0
,
$factor
[
'score'
]);
$this
->
assertLessThanOrEqual
(
100
,
$factor
[
'score'
]);
}
}
public
function
test_get_score_breakdown_returns_tier
()
:
void
{
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$this
->
assertInstanceOf
(
ReputationTier
::
class
,
$breakdown
[
'tier'
]);
}
// ─── Account Age Scoring ────────────────────────────────────────────
public
function
test_new_account_gets_low_age_score
()
:
void
{
$newUser
=
User
::
factory
()
->
creator
()
->
create
([
'created_at'
=>
now
()
->
subDays
(
5
)]);
$newCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$newUser
->
id
,
'last_active_at'
=>
now
(),
'completion_percentage'
=>
80
,
'avg_response_hours'
=>
4
,
]);
$this
->
service
->
calculateCreatorScore
(
$newCreator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$newCreator
);
$ageFactor
=
collect
(
$breakdown
[
'factors'
])
->
firstWhere
(
'key'
,
'account_age'
);
$this
->
assertLessThanOrEqual
(
20
,
$ageFactor
[
'score'
]);
}
public
function
test_old_account_gets_high_age_score
()
:
void
{
$oldUser
=
User
::
factory
()
->
creator
()
->
create
([
'created_at'
=>
now
()
->
subYears
(
3
)]);
$oldCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$oldUser
->
id
,
'last_active_at'
=>
now
(),
'completion_percentage'
=>
80
,
'avg_response_hours'
=>
4
,
]);
$this
->
service
->
calculateCreatorScore
(
$oldCreator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$oldCreator
);
$ageFactor
=
collect
(
$breakdown
[
'factors'
])
->
firstWhere
(
'key'
,
'account_age'
);
$this
->
assertEquals
(
100
,
$ageFactor
[
'score'
]);
}
// ─── Profile Completion Impact ──────────────────────────────────────
public
function
test_complete_profile_gives_max_profile_score
()
:
void
{
$this
->
creator
->
update
([
'completion_percentage'
=>
100
]);
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$profileFactor
=
collect
(
$breakdown
[
'factors'
])
->
firstWhere
(
'key'
,
'profile'
);
$this
->
assertEquals
(
100
,
$profileFactor
[
'score'
]);
}
public
function
test_empty_profile_gives_zero_profile_score
()
:
void
{
$this
->
creator
->
update
([
'completion_percentage'
=>
0
]);
$this
->
service
->
calculateCreatorScore
(
$this
->
creator
);
$breakdown
=
$this
->
service
->
getScoreBreakdown
(
$this
->
creator
);
$profileFactor
=
collect
(
$breakdown
[
'factors'
])
->
firstWhere
(
'key'
,
'profile'
);
$this
->
assertEquals
(
0
,
$profileFactor
[
'score'
]);
}
// ─── Recalculate All ────────────────────────────────────────────────
public
function
test_recalculate_all_updates_all_active_profiles
()
:
void
{
$this
->
creator
->
update
([
'reputation_score'
=>
null
]);
$this
->
company
->
update
([
'reputation_score'
=>
null
]);
$this
->
service
->
recalculateAll
();
$this
->
creator
->
refresh
();
$this
->
company
->
refresh
();
$this
->
assertNotNull
(
$this
->
creator
->
reputation_score
);
$this
->
assertNotNull
(
$this
->
company
->
reputation_score
);
}
public
function
test_recalculate_all_skips_inactive_users
()
:
void
{
$inactiveUser
=
User
::
factory
()
->
creator
()
->
create
([
'status'
=>
'suspended'
]);
$inactiveCreator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$inactiveUser
->
id
,
'reputation_score'
=>
null
,
]);
$this
->
service
->
recalculateAll
();
$inactiveCreator
->
refresh
();
$this
->
assertNull
(
$inactiveCreator
->
reputation_score
);
}
}
tests/Feature/VideoReview/VideoReviewServiceTest.php
0 → 100644
View file @
17007ee0
<?php
namespace
Tests\Feature\VideoReview
;
use
App\Models\User
;
use
App\Modules\Campaigns\Models\Campaign
;
use
App\Modules\Companies\Models\CompanyProfile
;
use
App\Modules\Creators\Models\CreatorProfile
;
use
App\Modules\Deliverables\Models\Deliverable
;
use
App\Modules\Deliverables\Models\Submission
;
use
App\Modules\Deliverables\Models\TimestampComment
;
use
App\Modules\Projects\Models\Project
;
use
App\Modules\VideoReview\Services\VideoReviewService
;
use
Illuminate\Foundation\Testing\RefreshDatabase
;
use
Tests\TestCase
;
class
VideoReviewServiceTest
extends
TestCase
{
use
RefreshDatabase
;
private
VideoReviewService
$service
;
private
User
$companyUser
;
private
User
$creatorUser
;
private
CreatorProfile
$creator
;
private
Submission
$submission
;
protected
function
setUp
()
:
void
{
parent
::
setUp
();
$this
->
service
=
app
(
VideoReviewService
::
class
);
$this
->
companyUser
=
User
::
factory
()
->
company
()
->
create
();
$company
=
CompanyProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
companyUser
->
id
]);
$this
->
creatorUser
=
User
::
factory
()
->
creator
()
->
create
();
$this
->
creator
=
CreatorProfile
::
factory
()
->
create
([
'user_id'
=>
$this
->
creatorUser
->
id
]);
$campaign
=
Campaign
::
factory
()
->
create
([
'company_id'
=>
$company
->
id
]);
$project
=
Project
::
factory
()
->
create
([
'campaign_id'
=>
$campaign
->
id
,
'company_id'
=>
$company
->
id
,
'creator_id'
=>
$this
->
creator
->
id
,
]);
$deliverable
=
Deliverable
::
factory
()
->
create
([
'project_id'
=>
$project
->
id
,
]);
$this
->
submission
=
Submission
::
create
([
'deliverable_id'
=>
$deliverable
->
id
,
'creator_id'
=>
$this
->
creator
->
id
,
'version'
=>
1
,
'notes'
=>
'First submission.'
,
'submitted_at'
=>
now
(),
]);
}
// ─── Add Timestamp Comment ──────────────────────────────────────────
public
function
test_add_timestamp_comment_creates_record
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
15.5
,
'comment'
=>
'The transition here is too abrupt.'
,
'type'
=>
'issue'
,
]);
$this
->
assertInstanceOf
(
TimestampComment
::
class
,
$comment
);
$this
->
assertEquals
(
$this
->
submission
->
id
,
$comment
->
submission_id
);
$this
->
assertEquals
(
$this
->
companyUser
->
id
,
$comment
->
user_id
);
$this
->
assertEquals
(
15.5
,
$comment
->
timestamp_seconds
);
$this
->
assertEquals
(
'The transition here is too abrupt.'
,
$comment
->
comment
);
$this
->
assertEquals
(
'issue'
,
$comment
->
type
);
$this
->
assertFalse
(
$comment
->
is_resolved
);
}
public
function
test_add_timestamp_comment_defaults_type_to_suggestion
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
30.0
,
'comment'
=>
'Maybe try a different angle here.'
,
]);
$this
->
assertEquals
(
'suggestion'
,
$comment
->
type
);
}
public
function
test_add_timestamp_comment_supports_all_types
()
:
void
{
$types
=
[
'issue'
,
'suggestion'
,
'praise'
];
foreach
(
$types
as
$type
)
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
"A
{
$type
}
comment."
,
'type'
=>
$type
,
]);
$this
->
assertEquals
(
$type
,
$comment
->
type
);
}
}
public
function
test_add_timestamp_comment_stores_timestamp_as_decimal
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
63.75
,
'comment'
=>
'At exactly 1:03.75'
,
]);
$this
->
assertEquals
(
63.75
,
(
float
)
$comment
->
timestamp_seconds
);
}
public
function
test_add_multiple_comments_on_same_submission
()
:
void
{
for
(
$i
=
0
;
$i
<
5
;
$i
++
)
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
$i
*
10.0
,
'comment'
=>
"Comment at
{
$i
}
0 seconds."
,
'type'
=>
'suggestion'
,
]);
}
$this
->
assertDatabaseCount
(
'timestamp_comments'
,
5
);
}
// ─── Resolve Comment ────────────────────────────────────────────────
public
function
test_resolve_comment_sets_resolved_flag
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Fix this.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
resolveComment
(
$comment
);
$comment
->
refresh
();
$this
->
assertTrue
(
$comment
->
is_resolved
);
$this
->
assertNotNull
(
$comment
->
resolved_at
);
}
public
function
test_resolve_comment_sets_resolved_at_timestamp
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
20.0
,
'comment'
=>
'Needs work.'
,
'type'
=>
'issue'
,
]);
$before
=
now
();
$this
->
service
->
resolveComment
(
$comment
);
$comment
->
refresh
();
$this
->
assertGreaterThanOrEqual
(
$before
->
timestamp
,
$comment
->
resolved_at
->
timestamp
);
}
// ─── Unresolve Comment ──────────────────────────────────────────────
public
function
test_unresolve_comment_clears_resolved_flag
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Fix this.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
resolveComment
(
$comment
);
$this
->
service
->
unresolveComment
(
$comment
);
$comment
->
refresh
();
$this
->
assertFalse
(
$comment
->
is_resolved
);
$this
->
assertNull
(
$comment
->
resolved_at
);
}
// ─── Get Comments for Submission ────────────────────────────────────
public
function
test_get_comments_ordered_by_timestamp
()
:
void
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
60.0
,
'comment'
=>
'Later comment.'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Earlier comment.'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
35.0
,
'comment'
=>
'Middle comment.'
,
]);
$comments
=
$this
->
service
->
getCommentsForSubmission
(
$this
->
submission
);
$timestamps
=
$comments
->
pluck
(
'timestamp_seconds'
)
->
map
(
fn
(
$t
)
=>
(
float
)
$t
)
->
toArray
();
$sorted
=
$timestamps
;
sort
(
$sorted
);
$this
->
assertEquals
(
$sorted
,
$timestamps
);
}
public
function
test_get_comments_eager_loads_user
()
:
void
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Test.'
,
]);
$comments
=
$this
->
service
->
getCommentsForSubmission
(
$this
->
submission
);
$this
->
assertTrue
(
$comments
->
first
()
->
relationLoaded
(
'user'
));
}
public
function
test_get_comments_returns_empty_for_no_comments
()
:
void
{
$comments
=
$this
->
service
->
getCommentsForSubmission
(
$this
->
submission
);
$this
->
assertCount
(
0
,
$comments
);
}
// ─── Get Unresolved Comments ────────────────────────────────────────
public
function
test_get_unresolved_comments_excludes_resolved
()
:
void
{
$resolved
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Fixed this.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
resolveComment
(
$resolved
);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
30.0
,
'comment'
=>
'Still needs work.'
,
'type'
=>
'issue'
,
]);
$unresolved
=
$this
->
service
->
getUnresolvedComments
(
$this
->
submission
);
$this
->
assertCount
(
1
,
$unresolved
);
$this
->
assertFalse
(
$unresolved
->
first
()
->
is_resolved
);
}
public
function
test_get_unresolved_comments_ordered_by_timestamp
()
:
void
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
50.0
,
'comment'
=>
'Later issue.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
5.0
,
'comment'
=>
'Earlier issue.'
,
'type'
=>
'issue'
,
]);
$unresolved
=
$this
->
service
->
getUnresolvedComments
(
$this
->
submission
);
$this
->
assertEquals
(
5.0
,
(
float
)
$unresolved
->
first
()
->
timestamp_seconds
);
}
// ─── Get Comments Grouped by Type ───────────────────────────────────
public
function
test_get_comments_grouped_by_type_returns_all_groups
()
:
void
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Bug here.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
20.0
,
'comment'
=>
'Maybe try this.'
,
'type'
=>
'suggestion'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
30.0
,
'comment'
=>
'This part is great!'
,
'type'
=>
'praise'
,
]);
$grouped
=
$this
->
service
->
getCommentsGroupedByType
(
$this
->
submission
);
$this
->
assertArrayHasKey
(
'issue'
,
$grouped
);
$this
->
assertArrayHasKey
(
'suggestion'
,
$grouped
);
$this
->
assertArrayHasKey
(
'praise'
,
$grouped
);
$this
->
assertCount
(
1
,
$grouped
[
'issue'
]);
$this
->
assertCount
(
1
,
$grouped
[
'suggestion'
]);
$this
->
assertCount
(
1
,
$grouped
[
'praise'
]);
}
public
function
test_get_comments_grouped_returns_empty_arrays_for_missing_types
()
:
void
{
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Bug.'
,
'type'
=>
'issue'
,
]);
$grouped
=
$this
->
service
->
getCommentsGroupedByType
(
$this
->
submission
);
$this
->
assertCount
(
1
,
$grouped
[
'issue'
]);
$this
->
assertCount
(
0
,
$grouped
[
'suggestion'
]);
$this
->
assertCount
(
0
,
$grouped
[
'praise'
]);
}
// ─── Get Comment Count ──────────────────────────────────────────────
public
function
test_get_comment_count_returns_all_counts
()
:
void
{
$issue
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'Issue.'
,
'type'
=>
'issue'
,
]);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
20.0
,
'comment'
=>
'Resolved issue.'
,
'type'
=>
'issue'
,
]);
$resolved
=
TimestampComment
::
orderByDesc
(
'id'
)
->
first
();
$this
->
service
->
resolveComment
(
$resolved
);
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
30.0
,
'comment'
=>
'Suggestion.'
,
'type'
=>
'suggestion'
,
]);
$this
->
submission
->
refresh
();
$counts
=
$this
->
service
->
getCommentCountForSubmission
(
$this
->
submission
);
$this
->
assertEquals
(
3
,
$counts
[
'total'
]);
$this
->
assertEquals
(
2
,
$counts
[
'unresolved'
]);
$this
->
assertEquals
(
1
,
$counts
[
'resolved'
]);
$this
->
assertEquals
(
1
,
$counts
[
'issues'
]);
}
// ─── Delete Comment ─────────────────────────────────────────────────
public
function
test_delete_comment_removes_from_database
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
10.0
,
'comment'
=>
'To be deleted.'
,
]);
$this
->
service
->
deleteComment
(
$comment
);
$this
->
assertDatabaseMissing
(
'timestamp_comments'
,
[
'id'
=>
$comment
->
id
]);
}
// ─── Model: Timestamp Formatted Accessor ────────────────────────────
public
function
test_timestamp_formatted_accessor
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
125.0
,
'comment'
=>
'At 2:05.'
,
]);
$this
->
assertEquals
(
'2:05'
,
$comment
->
timestamp_formatted
);
}
public
function
test_timestamp_formatted_at_zero
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
0
,
'comment'
=>
'At start.'
,
]);
$this
->
assertEquals
(
'0:00'
,
$comment
->
timestamp_formatted
);
}
public
function
test_timestamp_formatted_large_value
()
:
void
{
$comment
=
$this
->
service
->
addTimestampComment
(
$this
->
submission
,
$this
->
companyUser
,
[
'timestamp_seconds'
=>
3661.0
,
'comment'
=>
'Over an hour in.'
,
]);
$this
->
assertEquals
(
'61:01'
,
$comment
->
timestamp_formatted
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment