The vips driver uses jcupitt/vips (FFI binding), which needs ext-ffi
plus the libvips shared library at runtime - not the PECL vips
extension. Add a libvips apt install step and enable ffi in both test
workflows, then switch .env.testing to vips so CI exercises the new
default driver.
Guard ImageThumbnailSupersedeTest to skip when the configured driver's
runtime is unavailable, so contributors without libvips are not blocked.
- terms/privacy/community-guidelines views accessed the cached page as an
object ($page->title), but cachedPage() returns an array, causing a 500
whenever a custom Page row existed. Switch to array access.
- PublicRouteSmokeTest: enable open_registration before hitting /register,
which 404s by default when registration is disabled.
- Api/AccountTest: derive sanctum.stateful domain from app.url instead of
hardcoding pixelfed.test, so the first-party auth test is environment
independent.
ActivityPubDeliveryService::pool() and queueDelivery() only perform HTTP
delivery when app()->environment('production'); outside production they log
and return. The pool/job delivery tests were written before that guard was
added (commit 868e09b64) and never forced production, so no requests were
made and every assertSent/assertSentCount failed (6 failing tests).
Wrap the delivery-exercising tests in a forged production environment
(restored afterwards) and seed the public-ips DNS cache plus an empty
banned-domains cache so validateDestination() passes without a real DNS
lookup or DB query. Seed after factory creation, since the lazy DB refresh
can flush the cache store.
Also fix a by-value capture bug in the onError assertions: the $errors
array was captured through an arrow-fn wrapper, so the inner callback's
by-reference mutations never reached the assertion. Use a regular closure.
No production code changed; the production delivery gate is intended
behavior.
queueDelivery() runs synchronously from the v1 follow/unfollow endpoints
(via Helpers::sendSignedObject), which commit local state before delivery
and have no try/catch. After the Http::send() rewrite, a ConnectionException
from a momentarily-unreachable remote was rethrown out of queueDelivery(),
turning a best-effort delivery into a 500 for the user after the follow/
unfollow was already persisted. For unfollows, a retry then hit the
isFollowing==false branch and never re-sent the Undo, diverging state.
Treat transport failures (ConnectionException) as best-effort on this
single-delivery path: log, record host health, and return without
propagating. Other exception types (invalid sender/destination, signing,
serialization) still throw, matching pre-rewrite precondition behavior.
Also widen SendUpdateActor's per-user catch from HttpException to Throwable
so a single bad host no longer aborts a fleet-wide actor update (the old
HttpException catch is dead for ConnectionException/invalid-destination).
getEpochId() now computes a bounded epoch inline on cache miss (oldest
notification within the window) instead of returning 1, which would turn
every notification query into a full-table scan until the async pipeline
repopulates the cache. Adds coverage for the recent-id fallback and the
no-recent-notifications case. (Code change committed alongside the #7195
NotificationService edits.)
Notification status hydration compared item_type strictly against
Status::class (App\Models\Status). Rows created before the App\ ->
App\Models\ namespace migration store the legacy 'App\Status' morph-map
alias, so the comparison failed and favourite/comment/mention
notifications came back with no attached status. The web UI filters those
out client-side but keeps paginating (response never empty), leaving the
infinite-scroll loader spinning forever.
- NotificationTransformer + Mastodon NotificationTransformer: match both
the legacy alias and the current FQCN when hydrating status.
- NotificationService::buildNotification: same alias-aware deleted-item guard.
- NotificationService::getMaxPage/getMinPage: filter out unrenderable
notifications (status-type without a hydrated status) so the endpoint
never returns rows the UI discards, fixing pagination termination; warn
on unexpected notification types.
- Tests for transformer hydration (legacy + current), renderable filtering,
the unexpected-type warning, and pagination termination.
original_sha256 is the pre-optimization upload hash and can never match the
optimized local file, so comparing against it is not a valid integrity
check for migration. Drop the --verify-sha256 option and the checksum step
entirely; verify now relies on cloud-object existence and size parity.
original_sha256 is the hash of the file as originally uploaded, but the
async optimize pipeline (ImageResize/ImageUpdate) rewrites the local file
in place and never updates that column. Verifying the current local bytes
against it made every optimized image fail with sha256_mismatch, so the
migration reported moved=0 and exited 1.
Add a --verify-sha256 flag (off by default) that gates the checksum step.
By default verify relies on cloud-object existence and size parity, which
is the only signal that actually describes the uploaded copy. Add tests
covering both the default (migrates) and opt-in (fails) paths.
apiV1Add passed an absolute path to FFMpeg::open(), which laravel-ffmpeg
resolves relative to the local disk root, producing a doubled path and an
ffprobe 'Unable to probe / No such file or directory' failure.
Open the file via the disk instead: FFMpeg::fromDisk('local')->open($path)
for local storage, and an on-demand local disk rooted at the temp dir for
cloud storage.
Adds feature tests that pin the disk-relative path resolution.
publishNext() throws ValidationException::withMessages() for invalid
story overlays (bad text/hashtag/mention/url/type), but the enclosing
catch (\Exception $e) caught it too, since ValidationException extends
Exception. Clients got a generic 500 "Failed to create story" instead
of the intended 422 with field-level errors, and the log was flooded
with ordinary user-input validation failures logged as server errors.
Catch ValidationException separately, roll back, and rethrow it so
Laravel's exception handler renders the normal 422 response.
store() and update() open a manual DB::beginTransaction() but had early
return paths (duplicate keyword, keyword not found, per-filter keyword
limit) that hit neither DB::commit() nor the catch block's rollback,
leaving the transaction open past the end of the request. Add the
missing DB::rollBack() call on each early-exit branch, matching the
manual-transaction pattern used elsewhere in the app.
Aligns with the app's dominant convention (84 files vs 10). Avoids
running migrations for tests that never touch the database.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 2FA flow moved from a middleware-gated i/auth/checkpoint model to a
pending-login model (auth.pending session, POST /login/2fa, /login?step=2fa
challenge). The old tests referenced the removed route and dead session keys
(2fa.session.active, 2fa.attempts) and failed with 404s.
Rewritten against the new code as source of truth:
- Checkpoint test: throttle assertion retargeted to the login/2fa route;
failed-verification audit log now driven through a pending 2FA session.
- Logout-session test: asserts auth.pending is cleared and the user stays a
guest after MAX_2FA_ATTEMPTS failures (replacing the old flag cleanup).
- TwoFactorTest: challenge-redirect and challenge-page cases rewritten around
the login flow; setup/recovery password-confirmation cases unchanged.
- MiddlewarePipelineTest: 2FA is enforced at login, not per-request, so an
authenticated 2FA user browses normally.
Full suite: 715 passed.
- Suspended/missing user guards for get, increase, decrement, recalculate
- Staleness window boundary (fresh at N-1h, stale at N+1h)
- Sub-1000-byte rounding on increase/decrement (floor to KB)
- Command --stale filter (only recomputes stale/never-calculated users) and
missing --user id failure
- Migration dispatches the backfill job to the low queue (Bus::fake)
Repair accounts whose storage counter drifted before the self-heal logic
existed (#7169). A data migration dispatches RecalculateAllUserStoragePipeline
to the low queue so the deploy is not blocked while every user is recomputed
from source. The job is unique and idempotent, so re-runs are harmless.
- RecalculateAllUserStoragePipeline: chunked recalc of all active users
- Migration dispatches the job (no inline heavy work during deploy)
- Test covers bulk recalculation from actual media
UserStorageService::get() now recalculates from source when the cached
counter is missing or older than STALE_AFTER_HOURS, instead of returning a
possibly-inflated cached value. This is what unblocks a user stuck at the
account size limit: the limit check on their next upload attempt reads the
freshly recalculated real usage rather than the drifted value (#7169).
The upload flow reads get() and enforces the limit BEFORE the write-path
heal runs, so a blocked user could never self-heal via upload/delete alone.
Healing on read closes that gap and makes the scheduled reconciler a
belt-and-suspenders safety net rather than a requirement.
A fresh counter is still trusted as-is (no per-read SUM). Adds tests for the
stale-get recompute and fresh-get trust paths.
Make increaseStorageUsed/decrementStorageUsed recalculate from source when
the cached counter is older than STALE_AFTER_HOURS (168h) or never
calculated, so an affected user is corrected the next time they upload or
delete without waiting for the nightly reconciler. Callers save/delete the
media row before calling these, so the from-source recalc already reflects
the change and the incremental delta is skipped on the recalc path.
- Add UserStorageService::STALE_AFTER_HOURS and isStale() helper (no extra
query: reads the already-loaded model), with defensive Carbon parsing
- Cast users.storage_used_updated_at to datetime so freshness comparisons
work on a Carbon instance
- Add tests for stale/fresh/never-calculated increase and decrement paths
users.storage_used only ever grew: uploads incremented it but no deletion
path decremented it, so users hit the account size limit even when their
real media usage was well below it.
- Decrement storage_used in MediaDeletePipeline when media is removed
- Add UserStorageService::increaseStorageUsed / decrementStorageUsed as the
fast, symmetric hot-path counter updates (floor-based, clamped at zero)
- Refactor the 6 upload call sites to use increaseStorageUsed instead of
duplicated inline writes (also fixes ceil/floor drift vs the reconciler)
- Add (user_id, size) covering index so per-user SUM(size) is not a full
table scan (INPLACE/LOCK=NONE, skipped on sqlite)
- Add user:storage:recalculate command to repair affected accounts, with a
daily --stale=168 scheduled reconciler to correct any drift
- Add regression tests for the pipeline and UserStorageService