Skip to content

fix(cdn): resolve the eight, and one of them was real - #11

Merged
Snider merged 1 commit into
mainfrom
fix/cdn-eight
Aug 8, 2026
Merged

fix(cdn): resolve the eight, and one of them was real#11
Snider merged 1 commit into
mainfrom
fix/cdn-eight

Conversation

@Snider

@Snider Snider commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The eight that survived #10 were real disagreements rather than one crash worn eight times, so each got decided on its own evidence. Six were the test being wrong, one was the harness, and one was the code.

The code one

CdnUrlBuilder::buildSignedUrlBase() is typed : string and passed config('cdn.bunny.private.pull_zone') straight into str_starts_with(). Unconfigured that is null, and str_starts_with(null, …) is a TypeError on PHP 8 — so signing with a token set and no pull zone configured didn't fail politely, it threw from inside the URL builder.

Only reachable once a token exists, which is why no test had ever got there: the test that would have was setting the wrong config key and taking the empty-token early return instead. Fixing the test found the bug.

Also code — a consistency defect

withVersion() emitted id= while Core\Helpers\Cdn::versioned() — the helper applications actually call from templates — emits v=. The same package cache-busted the same assets under two parameter names depending on which door you came in by.

Nothing required id: no CDN documentation here mentions it, every docblock names the purpose rather than the parameter, and its only appearance in the history is an unrelated Rector pass. Now v. One deploy's worth of cache misses, which is what a version parameter is for.

The harness one

cdn.paths was absent — the base TestCase registers only LifecycleEventProvider and nothing merges the package's cdn config, so pathPrefix() fell back to the raw category name. 'avatar' stayed 'avatar' where the config maps it to 'avatars'. The test was right all along.

Deliberately one key, not the file: merging the whole config overrode the test disks configured below it and took the file from 6 failures to 9. I did that first and reverted it.

Two further cases needed the Config package's migrations — BunnyStorageService reads zone credentials through ConfigService, which is a query against config_resolved, so isConfigured() died on "no such table" rather than on anything it was testing.

The stale tests — four, each pinning something the code has never done

test pinned actual
signed() cdn.signing_key, cdn.token_lifetime reads cdn.bunny.private.token; neither of those keys is read anywhere
urls() cdn_url / origin_url contract is cdn / origin, as allUrls() documents
copy() destination path as source bucket, then two disk names every argument after the first in the wrong parameter — written against a signature this method has not had
size() fake()->create('test.txt', 50) reports 51200 bytes, writes zero

The last one measured rather than guessed:

create_getSize  51200      create_realBytes  0
image_getSize     695      image_realBytes  695

So the stored file was empty and size() answered 0 correctly — the assertion was measuring the fixture. Now createWithContent with known content, asserting the exact size rather than "more than nothing".

Receipts

CdnIntegrationTest    8 failed / 22 passed  ->  30 passed, 0 failed
Module suite          426 -> 418 failed,  330 -> 338 passed
gate suites           268 passed, 0 failed — unchanged
pint, phpstan         clean

🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io

The eight that survived #10 were real disagreements rather than one crash worn
eight times, so each got decided on its own evidence. Six were the test being
wrong, one was the harness, and one was the code.

THE CODE ONE. CdnUrlBuilder::buildSignedUrlBase() is typed to return string and
passed config('cdn.bunny.private.pull_zone') straight into str_starts_with().
Unconfigured, that is null, and str_starts_with(null, …) is a TypeError on PHP 8 —
so signing with a token set and no pull zone did not fail politely, it threw from
inside the URL builder. Only reachable once a token exists, which is why no test
had ever got there: the test that would have was setting the wrong config key and
taking the empty-token early return instead. Cast and defaulted.

Also code, and a consistency defect rather than a crash: withVersion() emitted
`id=` while Core\Helpers\Cdn::versioned() — the helper applications actually call
from templates — emits `v=`. The same package cache-busted the same assets under
two parameter names depending on which door you came in by. Nothing required
`id`: no CDN documentation here mentions it, every docblock names the purpose and
not the parameter, and its only appearance in the history is an unrelated Rector
pass. Now `v`, consistent with the helper and with the test. One deploy's worth of
cache misses, which is what a version parameter is for.

THE HARNESS ONE. cdn.paths was absent, because the base TestCase registers only
LifecycleEventProvider and nothing merges the package's cdn config. pathPrefix()
falls back to the raw category name, so 'avatar' stayed 'avatar' where the config
maps it to 'avatars' — the test was right all along. Set that one key.

Deliberately one key and not the file: merging the whole config wholesale
overrode the test disks configured below it and took the file from 6 failures to
9. I did that first and reverted it.

Two more needed the Config package's migrations. BunnyStorageService reads its
zone credentials through ConfigService, which is a database query against
config_resolved, so isConfigured() died on "no such table" rather than on anything
it was testing.

THE STALE TESTS, four of them, each pinning something the code has never done:

  signed()   set cdn.signing_key and cdn.token_lifetime; the implementation reads
             cdn.bunny.private.token, and neither of those keys is read anywhere
  urls()     asserted cdn_url / origin_url; the contract is cdn / origin, which is
             what allUrls() documents too
  copy()     passed the destination path as the source bucket and two disk names
             after it — every argument after the first in the wrong parameter,
             written against a signature this method has not had
  size()     used UploadedFile::fake()->create('test.txt', 50), which reports
             getSize() 51200 and writes zero real bytes. Measured, not guessed:
             create_getSize 51200 / create_realBytes 0, against image_getSize 695
             / image_realBytes 695. So the stored file was empty and size()
             answered 0 correctly — the assertion was measuring the fixture.
             Now createWithContent with known content, asserting the exact size
             rather than "more than nothing".

  CdnIntegrationTest       8 failed / 22 passed  ->  30 passed, 0 failed
  Module suite             426 -> 418 failed,  330 -> 338 passed
  gate suites              268 passed, 0 failed — unchanged
  pint, phpstan            clean

Co-Authored-By: Virgil <virgil@lethean.io>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@Snider, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 18a8c687-12ba-4a43-8f19-b6e13d4bb613

📥 Commits

Reviewing files that changed from the base of the PR and between 6f26fa9 and fcedfb2.

📒 Files selected for processing (2)
  • src/Core/Cdn/Services/CdnUrlBuilder.php
  • src/Core/Tests/Feature/CdnIntegrationTest.php

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Snider
Snider merged commit 5551b59 into main Aug 8, 2026
13 of 14 checks passed
@Snider
Snider deleted the fix/cdn-eight branch August 8, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant