setAccessible(true); return $method->invoke( $service, 'pixelfed.org', 443, ['203.0.113.50'], 2000000, ); } it('does not pass the forbidden curl protocol constants Guzzle 8 rejects', function () { $options = buildSecureFetchOptions(); expect($options['curl'])->not->toHaveKey(CURLOPT_PROTOCOLS); expect($options['curl'])->not->toHaveKey(CURLOPT_REDIR_PROTOCOLS); }); it('restricts the scheme to https via the native protocols option', function () { $options = buildSecureFetchOptions(); expect($options['protocols'])->toBe(['https']); expect($options['allow_redirects'])->toBeFalse(); }); it('keeps the resolve pin and connection-reuse hardening', function () { $options = buildSecureFetchOptions(); expect($options['curl'][CURLOPT_RESOLVE])->toBe(['pixelfed.org:443:203.0.113.50']); expect($options['curl'][CURLOPT_FRESH_CONNECT])->toBeTrue(); expect($options['curl'][CURLOPT_FORBID_REUSE])->toBeTrue(); }); it('never combines stream mode with a non-empty curl option', function () { $options = buildSecureFetchOptions(); // Guzzle 8's stream handler rejects any non-empty curl option, so the // CURLOPT_RESOLVE pin can only ride a buffered (non-stream) request. expect($options['curl'])->not->toBeEmpty(); expect($options['stream'])->toBeFalse(); }); it('issues the ranged probe GET without stream mode', function () { Cache::flush(); Cache::put('helpers:url:public-ips:'.hash('xxh128', 'pixelfed.org'), ['203.0.113.50'], 3600); Http::fake(function ($request) { if ($request->method() === 'HEAD') { return Http::response('', 403); } return Http::response('x', 206, [ 'Content-Type' => 'image/jpeg', 'Content-Range' => 'bytes 0-0/54321', 'Content-Length' => '1', ]); }); $head = SecureMediaFetchService::head('https://pixelfed.org/avatar.jpeg', 2000000); expect($head)->toBe(['length' => 54321, 'mime' => 'image/jpeg']); Http::assertSent(function ($request) { return $request->method() === 'GET' && $request->hasHeader('Range', 'bytes=0-0'); }); });