Skip to content

Commit cc0156e

Browse files
Countdown banner
1 parent c1e70a7 commit cc0156e

4 files changed

Lines changed: 421 additions & 2 deletions

File tree

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
# PR #54 Review Fixes Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Fix issues identified in PR #54 code review comments and add tests for identified coverage gaps.
6+
7+
**Architecture:** Fix the SponsorControllerTest to avoid S3 dependency by using HTTP logo URLs in the factory, clean up redundant code in attach() calls and RefreshDatabase import, and add new tests for the Sponsor `logoUrl` accessor and model relationships.
8+
9+
**Tech Stack:** Laravel 12, Pest PHP, SQLite (test), PHP 8.4
10+
11+
---
12+
13+
## Review Comment Triage
14+
15+
### Will Fix
16+
1. **S3 dependency in tests (CodeRabbit - Major)** — Factory generates relative logo paths triggering `Storage::disk('s3')->url()`. Fix by adding a `withTestLogo()` factory state that uses an HTTP URL.
17+
2. **Redundant `uses(RefreshDatabase::class)` (Copilot)** — Already applied globally in `tests/Pest.php:15`. Remove.
18+
3. **Redundant pivot attributes in `attach()` (Copilot + CodeRabbit)** — Remove `sponsor_uuid` and `conference_uuid` from attach calls; the relationship handles them.
19+
20+
### Will NOT Fix (with justification)
21+
1. **Migration missing `down()` method** — Project convention in CLAUDE.md: "do not write down methods in migrations, only up methods."
22+
2. **Migration `->change()` without doctrine/dbal** — False positive. Laravel 11+ handles `->change()` natively without doctrine/dbal.
23+
3. **Dynamic property `$this->conference` PHP 8.4 deprecation** — Standard Pest PHP pattern. Pest's test case handles dynamic properties via magic methods. This is idiomatic Pest code.
24+
4. **File path doesn't match behavior tested** — Low-priority cosmetic change. Would rename file but not change behavior. Deferring to avoid unnecessary churn.
25+
5. **fwallen's `sponsor.websiteEl` comment** — The code at lines 162-171 already uses a local `websiteEl` variable correctly. The code is `websiteEl.href = sponsor.website` which is correct. Eric confirmed in the thread.
26+
27+
## File Structure
28+
29+
| File | Action | Responsibility |
30+
|------|--------|----------------|
31+
| `tests/Feature/Http/Controllers/SponsorControllerTest.php` | Modify | Fix S3 dep, remove redundant RefreshDatabase, clean attach() calls |
32+
| `database/factories/SponsorFactory.php` | Modify | Add `withTestLogo()` state for tests |
33+
| `tests/Feature/SponsorModelTest.php` | Create | Test `logoUrl` accessor edge cases |
34+
| `tests/Feature/RelationshipTest.php` | Create | Test Conference-Sponsor and Conference-User relationships |
35+
36+
---
37+
38+
### Task 1: Add `withTestLogo()` Factory State
39+
40+
**Files:**
41+
- Modify: `database/factories/SponsorFactory.php:46`
42+
43+
- [ ] **Step 1: Add the factory state method**
44+
45+
Add after the `withoutWebsite()` method:
46+
47+
```php
48+
/**
49+
* Create a sponsor with an HTTP logo URL (avoids S3 dependency in tests).
50+
*/
51+
public function withTestLogo(): static
52+
{
53+
return $this->state(fn (array $attributes) => [
54+
'logo' => 'https://example.com/logos/test-logo.png',
55+
]);
56+
}
57+
```
58+
59+
- [ ] **Step 2: Verify factory still works**
60+
61+
Run: `php artisan tinker --execute="echo App\Models\Sponsor::factory()->withTestLogo()->make()->logo_url;"`
62+
Expected: `https://example.com/logos/test-logo.png`
63+
64+
- [ ] **Step 3: Commit**
65+
66+
```bash
67+
git add database/factories/SponsorFactory.php
68+
git commit -m "feat: add withTestLogo factory state to avoid S3 dependency in tests"
69+
```
70+
71+
---
72+
73+
### Task 2: Fix SponsorControllerTest
74+
75+
**Files:**
76+
- Modify: `tests/Feature/Http/Controllers/SponsorControllerTest.php`
77+
78+
- [ ] **Step 1: Remove redundant RefreshDatabase import and usage**
79+
80+
Remove line 5 (`use Illuminate\Foundation\Testing\RefreshDatabase;`) and line 7 (`uses(RefreshDatabase::class);`). This is already applied globally in `tests/Pest.php:14-16`.
81+
82+
- [ ] **Step 2: Update all `Sponsor::factory()` calls to chain `->withTestLogo()`**
83+
84+
Every `Sponsor::factory()` call must include `->withTestLogo()` to avoid S3:
85+
86+
```php
87+
// Line 25 - sponsor with website
88+
$sponsor = Sponsor::factory()->withTestLogo()->create([
89+
'website' => 'https://example.com',
90+
]);
91+
92+
// Line 43 - sponsor without website
93+
$sponsor = Sponsor::factory()->withTestLogo()->withoutWebsite()->create();
94+
95+
// Line 58 - sponsor with site
96+
$sponsorWithSite = Sponsor::factory()->withTestLogo()->create([
97+
'website' => 'https://has-website.com',
98+
]);
99+
100+
// Line 62 - sponsor without site
101+
$sponsorWithoutSite = Sponsor::factory()->withTestLogo()->withoutWebsite()->create();
102+
```
103+
104+
- [ ] **Step 3: Clean up all `attach()` calls to remove redundant pivot keys**
105+
106+
Replace each attach call. Only pass `sponsorship_level` — Laravel fills the foreign keys automatically.
107+
108+
Before:
109+
```php
110+
$this->conference->sponsors()->attach($sponsor->uuid, [
111+
'sponsor_uuid' => $sponsor->uuid,
112+
'conference_uuid' => $this->conference->uuid,
113+
'sponsorship_level' => 'gold',
114+
]);
115+
```
116+
117+
After:
118+
```php
119+
$this->conference->sponsors()->attach($sponsor->uuid, [
120+
'sponsorship_level' => 'gold',
121+
]);
122+
```
123+
124+
Apply to all 4 attach calls (lines 29, 45, 64, 70).
125+
126+
- [ ] **Step 4: Run tests to verify they pass**
127+
128+
Run: `php artisan test --filter=SponsorControllerTest`
129+
Expected: 3 tests pass, 0 failures
130+
131+
- [ ] **Step 5: Commit**
132+
133+
```bash
134+
git add tests/Feature/Http/Controllers/SponsorControllerTest.php
135+
git commit -m "fix: remove S3 dependency, redundant RefreshDatabase, and redundant pivot keys in sponsor tests"
136+
```
137+
138+
---
139+
140+
### Task 3: Add Sponsor `logoUrl` Accessor Tests
141+
142+
**Files:**
143+
- Create: `tests/Feature/SponsorModelTest.php`
144+
145+
- [ ] **Step 1: Create the test file**
146+
147+
Run: `php artisan make:test SponsorModelTest --pest`
148+
149+
- [ ] **Step 2: Write tests for all logoUrl branches**
150+
151+
The `logoUrl` accessor in `app/Models/Sponsor.php:62-87` has 4 branches:
152+
1. No logo → returns null
153+
2. HTTP URL logo → returns as-is
154+
3. Local file exists → returns asset URL
155+
4. Relative path (S3 fallback) → calls Storage::disk('s3')
156+
157+
```php
158+
<?php
159+
160+
use App\Models\Sponsor;
161+
use Illuminate\Support\Facades\Storage;
162+
163+
test('logoUrl returns null when logo is empty', function () {
164+
$sponsor = Sponsor::factory()->make(['logo' => null]);
165+
166+
expect($sponsor->logo_url)->toBeNull();
167+
});
168+
169+
test('logoUrl returns http url as-is', function () {
170+
$sponsor = Sponsor::factory()->make([
171+
'logo' => 'https://example.com/logo.png',
172+
]);
173+
174+
expect($sponsor->logo_url)->toBe('https://example.com/logo.png');
175+
});
176+
177+
test('logoUrl returns asset url for existing public file', function () {
178+
$sponsor = Sponsor::factory()->make([
179+
'logo' => 'favicon.ico',
180+
]);
181+
182+
expect($sponsor->logo_url)->toContain('favicon.ico');
183+
});
184+
185+
test('logoUrl falls back to s3 for non-public relative paths', function () {
186+
Storage::fake('s3');
187+
188+
$sponsor = Sponsor::factory()->make([
189+
'logo' => 'vendor_logos/some-logo.png',
190+
]);
191+
192+
$url = $sponsor->logo_url;
193+
194+
expect($url)->toContain('vendor_logos/some-logo.png');
195+
});
196+
197+
test('logoUrl prepends vendor_logos prefix for bare paths on s3', function () {
198+
Storage::fake('s3');
199+
200+
$sponsor = Sponsor::factory()->make([
201+
'logo' => 'some-logo.png',
202+
]);
203+
204+
$url = $sponsor->logo_url;
205+
206+
expect($url)->toContain('vendor_logos/some-logo.png');
207+
});
208+
```
209+
210+
- [ ] **Step 3: Run the tests**
211+
212+
Run: `php artisan test tests/Feature/SponsorModelTest.php`
213+
Expected: 5 tests pass
214+
215+
- [ ] **Step 4: Commit**
216+
217+
```bash
218+
git add tests/Feature/SponsorModelTest.php
219+
git commit -m "test: add comprehensive tests for Sponsor logoUrl accessor"
220+
```
221+
222+
---
223+
224+
### Task 4: Add Model Relationship Tests
225+
226+
**Files:**
227+
- Create: `tests/Feature/RelationshipTest.php`
228+
229+
- [ ] **Step 1: Create the test file**
230+
231+
Run: `php artisan make:test RelationshipTest --pest`
232+
233+
- [ ] **Step 2: Write relationship tests**
234+
235+
```php
236+
<?php
237+
238+
use App\Models\Conference;
239+
use App\Models\Sponsor;
240+
use App\Models\User;
241+
242+
test('conference has many sponsors through pivot', function () {
243+
$conference = Conference::create([
244+
'uuid' => 'rel-test-conf-uuid',
245+
'name' => 'Relationship Test Conference',
246+
'venue_name' => 'Test Venue',
247+
'start_date' => '2026-05-19',
248+
'end_date' => '2026-05-21',
249+
]);
250+
251+
$sponsor = Sponsor::factory()->withTestLogo()->create();
252+
253+
$conference->sponsors()->attach($sponsor->uuid, [
254+
'sponsorship_level' => 'gold',
255+
]);
256+
257+
$conference->refresh();
258+
259+
expect($conference->sponsors)->toHaveCount(1);
260+
expect($conference->sponsors->first()->name)->toBe($sponsor->name);
261+
expect($conference->sponsors->first()->pivot->sponsorship_level)->toBe('gold');
262+
});
263+
264+
test('sponsor belongs to many conferences through pivot', function () {
265+
$conference = Conference::create([
266+
'uuid' => 'rel-test-conf-uuid-2',
267+
'name' => 'Reverse Relationship Conference',
268+
'venue_name' => 'Test Venue',
269+
'start_date' => '2026-05-19',
270+
'end_date' => '2026-05-21',
271+
]);
272+
273+
$sponsor = Sponsor::factory()->withTestLogo()->create();
274+
275+
$conference->sponsors()->attach($sponsor->uuid, [
276+
'sponsorship_level' => 'silver',
277+
]);
278+
279+
$sponsor->refresh();
280+
281+
expect($sponsor->conferences)->toHaveCount(1);
282+
expect($sponsor->conferences->first()->name)->toBe($conference->name);
283+
expect($sponsor->conferences->first()->pivot->sponsorship_level)->toBe('silver');
284+
});
285+
286+
test('conference has many users through pivot', function () {
287+
$conference = Conference::create([
288+
'uuid' => 'rel-test-conf-uuid-3',
289+
'name' => 'User Relationship Conference',
290+
'venue_name' => 'Test Venue',
291+
'start_date' => '2026-05-19',
292+
'end_date' => '2026-05-21',
293+
]);
294+
295+
$user = User::factory()->create();
296+
297+
$conference->users()->attach($user->uuid);
298+
299+
$conference->refresh();
300+
301+
expect($conference->users)->toHaveCount(1);
302+
expect($conference->users->first()->email)->toBe($user->email);
303+
});
304+
305+
test('conference can have multiple sponsors at different levels', function () {
306+
$conference = Conference::create([
307+
'uuid' => 'rel-test-conf-uuid-4',
308+
'name' => 'Multi Sponsor Conference',
309+
'venue_name' => 'Test Venue',
310+
'start_date' => '2026-05-19',
311+
'end_date' => '2026-05-21',
312+
]);
313+
314+
$goldSponsor = Sponsor::factory()->withTestLogo()->create();
315+
$silverSponsor = Sponsor::factory()->withTestLogo()->create();
316+
317+
$conference->sponsors()->attach($goldSponsor->uuid, [
318+
'sponsorship_level' => 'gold',
319+
]);
320+
321+
$conference->sponsors()->attach($silverSponsor->uuid, [
322+
'sponsorship_level' => 'silver',
323+
]);
324+
325+
$conference->refresh();
326+
327+
expect($conference->sponsors)->toHaveCount(2);
328+
329+
$levels = $conference->sponsors->pluck('pivot.sponsorship_level')->toArray();
330+
expect($levels)->toContain('gold');
331+
expect($levels)->toContain('silver');
332+
});
333+
```
334+
335+
- [ ] **Step 3: Run the tests**
336+
337+
Run: `php artisan test tests/Feature/RelationshipTest.php`
338+
Expected: 4 tests pass
339+
340+
- [ ] **Step 4: Commit**
341+
342+
```bash
343+
git add tests/Feature/RelationshipTest.php
344+
git commit -m "test: add model relationship tests for Conference, Sponsor, and User"
345+
```
346+
347+
---
348+
349+
### Task 5: Run Full Test Suite
350+
351+
- [ ] **Step 1: Run all tests**
352+
353+
Run: `php artisan test`
354+
Expected: All tests pass (existing + new)
355+
356+
- [ ] **Step 2: Run Pint for formatting**
357+
358+
Run: `vendor/bin/pint --dirty`
359+
360+
- [ ] **Step 3: Final commit if Pint made changes**
361+
362+
```bash
363+
git add -A
364+
git commit -m "style: apply Pint formatting"
365+
```

0 commit comments

Comments
 (0)